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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2018-02-28 22:35:22 +0300
committerKamil Trzciński <ayufan@ayufan.eu>2018-02-28 22:35:22 +0300
commite0401df1214397626e65e58166988fe62715d372 (patch)
tree087d8ca4a1611aa50a8ac98e66f7d1657ff1f90f
parent2b7b60728426c10ef1188a1073d3630805773a35 (diff)
parent11c67e7c2f992299ff5918ce67995b73d1e0be6d (diff)
Merge commit '11c67e7c2f992299ff5918ce67995b73d1e0be6d' into object-storage-ee-to-ce-backport
-rw-r--r--.eslintignore3
-rw-r--r--.flayignore1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml63
-rw-r--r--.rubocop.yml4
-rw-r--r--CHANGELOG.md244
-rw-r--r--CONTRIBUTING.md25
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION3
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile19
-rw-r--r--Gemfile.lock110
-rw-r--r--LICENSE6
-rw-r--r--PROCESS.md5
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/auth_buttons/signin_with_google.pngbin0 -> 3983 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/favicon-blue.icobin5430 -> 5430 bytes
-rw-r--r--app/assets/images/icon_image_comment.svg1
-rw-r--r--app/assets/images/icon_image_comment@2x.svg1
-rw-r--r--app/assets/images/icons.json1
-rw-r--r--app/assets/images/icons.svg1
-rw-r--r--app/assets/images/illustrations/issues.svg1
-rw-r--r--app/assets/images/illustrations/labels.svg1
-rw-r--r--app/assets/images/illustrations/merge_requests.svg1
-rw-r--r--app/assets/images/illustrations/monitoring/getting_started.svg (renamed from app/views/shared/empty_states/monitoring/_getting_started.svg)0
-rw-r--r--app/assets/images/illustrations/monitoring/loading.svg (renamed from app/views/shared/empty_states/monitoring/_loading.svg)0
-rw-r--r--app/assets/images/illustrations/monitoring/unable_to_connect.svg (renamed from app/views/shared/empty_states/monitoring/_unable_to_connect.svg)0
-rw-r--r--app/assets/images/illustrations/pipelines_empty.svg1
-rw-r--r--app/assets/images/illustrations/pipelines_failed.svg1
-rw-r--r--app/assets/images/illustrations/priority_labels.svg1
-rw-r--r--app/assets/images/illustrations/todos_all_done.svg1
-rw-r--r--app/assets/images/illustrations/todos_empty.svg1
-rw-r--r--app/assets/images/new_nav.pngbin14322 -> 0 bytes
-rw-r--r--app/assets/images/old_nav.pngbin25617 -> 0 bytes
-rw-r--r--app/assets/javascripts/abuse_reports.js5
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js5
-rw-r--r--app/assets/javascripts/awards_handler.js53
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js5
-rw-r--r--app/assets/javascripts/blob/notebook/index.js4
-rw-r--r--app/assets/javascripts/blob/pdf/index.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js4
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js6
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js10
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js6
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js21
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js3
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js8
-rw-r--r--app/assets/javascripts/build.js5
-rw-r--r--app/assets/javascripts/build_artifacts.js24
-rw-r--r--app/assets/javascripts/clusters.js112
-rw-r--r--app/assets/javascripts/commit.js12
-rw-r--r--app/assets/javascripts/commit/file.js14
-rw-r--r--app/assets/javascripts/commit/image_file.js20
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue10
-rw-r--r--app/assets/javascripts/commits.js51
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/commons/polyfills/custom_event.js7
-rw-r--r--app/assets/javascripts/commons/polyfills/event.js18
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js3
-rw-r--r--app/assets/javascripts/copy_as_gfm.js67
-rw-r--r--app/assets/javascripts/create_label.js29
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue55
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue26
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js51
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.vue51
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_component.vue57
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js52
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js53
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue60
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js52
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js62
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue66
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js53
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue59
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js49
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue60
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js25
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.vue29
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js103
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js35
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js5
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue2
-rw-r--r--app/assets/javascripts/diff.js23
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js11
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js2
-rw-r--r--app/assets/javascripts/dispatcher.js27
-rw-r--r--app/assets/javascripts/dropzone_input.js9
-rw-r--r--app/assets/javascripts/environments/components/environment.vue37
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue20
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js6
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js61
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js57
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_options.js12
-rw-r--r--app/assets/javascripts/files_comment_button.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js19
-rw-r--r--app/assets/javascripts/fly_out_nav.js7
-rw-r--r--app/assets/javascripts/gl_dropdown.js7
-rw-r--r--app/assets/javascripts/groups/components/groups.vue5
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js3
-rw-r--r--app/assets/javascripts/groups/index.js7
-rw-r--r--app/assets/javascripts/groups/stores/groups_store.js5
-rw-r--r--app/assets/javascripts/groups_select.js3
-rw-r--r--app/assets/javascripts/help/help.js6
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js38
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js58
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js44
-rw-r--r--app/assets/javascripts/image_diff/helpers/index.js25
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js95
-rw-r--r--app/assets/javascripts/image_diff/image_badge.js23
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js143
-rw-r--r--app/assets/javascripts/image_diff/init_discussion_tab.js12
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js92
-rw-r--r--app/assets/javascripts/image_diff/view_types.js9
-rw-r--r--app/assets/javascripts/issuable_context.js5
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue7
-rw-r--r--app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue23
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue4
-rw-r--r--app/assets/javascripts/issue_show/index.js1
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js1
-rw-r--r--app/assets/javascripts/labels_select.js10
-rw-r--r--app/assets/javascripts/lib/utils/animate.js49
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js861
-rw-r--r--app/assets/javascripts/lib/utils/csrf.js58
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/image_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/poll.js3
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js107
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js30
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js20
-rw-r--r--app/assets/javascripts/line_highlighter.js285
-rw-r--r--app/assets/javascripts/locale/index.js27
-rw-r--r--app/assets/javascripts/locale/sprintf.js26
-rw-r--r--app/assets/javascripts/main.js28
-rw-r--r--app/assets/javascripts/merge_request.js15
-rw-r--r--app/assets/javascripts/merge_request_tabs.js19
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue78
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue62
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue69
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue15
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_paths.vue)0
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js22
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js5
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js50
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js1
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js92
-rw-r--r--app/assets/javascripts/new_sidebar.js21
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue30
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue28
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue14
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue14
-rw-r--r--app/assets/javascripts/notebook/cells/output/image.vue16
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue18
-rw-r--r--app/assets/javascripts/notebook/cells/prompt.vue16
-rw-r--r--app/assets/javascripts/notebook/index.vue22
-rw-r--r--app/assets/javascripts/notes.js158
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue46
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue19
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/issue_note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/issue_note_form.vue20
-rw-r--r--app/assets/javascripts/notes/components/issue_note_icons.js37
-rw-r--r--app/assets/javascripts/notes/components/issue_system_note.vue9
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js15
-rw-r--r--app/assets/javascripts/notes/stores/actions.js13
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js22
-rw-r--r--app/assets/javascripts/pager.js4
-rw-r--r--app/assets/javascripts/pdf/index.vue20
-rw-r--r--app/assets/javascripts/pdf/page/index.vue14
-rw-r--r--app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js4
-rw-r--r--app/assets/javascripts/pipelines.js3
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue6
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js6
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue146
-rw-r--r--app/assets/javascripts/profile/account/index.js21
-rw-r--r--app/assets/javascripts/profile/gl_crop.js3
-rw-r--r--app/assets/javascripts/profile/profile.js3
-rw-r--r--app/assets/javascripts/project_fork.js18
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js2
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js51
-rw-r--r--app/assets/javascripts/registry/components/app.vue62
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue131
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue137
-rw-r--r--app/assets/javascripts/registry/constants.js15
-rw-r--r--app/assets/javascripts/registry/index.js25
-rw-r--r--app/assets/javascripts/registry/stores/actions.js39
-rw-r--r--app/assets/javascripts/registry/stores/getters.js2
-rw-r--r--app/assets/javascripts/registry/stores/index.js39
-rw-r--r--app/assets/javascripts/registry/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js54
-rw-r--r--app/assets/javascripts/repo/components/repo.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue6
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue10
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue16
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue62
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js20
-rw-r--r--app/assets/javascripts/repo/index.js4
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js10
-rw-r--r--app/assets/javascripts/right_sidebar.js42
-rw-r--r--app/assets/javascripts/search_autocomplete.js14
-rw-r--r--app/assets/javascripts/shortcuts.js16
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue61
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue120
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js17
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js4
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js82
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js1
-rw-r--r--app/assets/javascripts/single_file_diff.js7
-rw-r--r--app/assets/javascripts/todos.js20
-rw-r--r--app/assets/javascripts/users_select.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js48
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js31
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue10
-rw-r--r--app/assets/javascripts/vue_shared/mixins/issuable.js9
-rw-r--r--app/assets/javascripts/vue_shared/translate.js2
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js5
-rw-r--r--app/assets/stylesheets/framework.scss5
-rw-r--r--app/assets/stylesheets/framework/animations.scss3
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/blocks.scss22
-rw-r--r--app/assets/stylesheets/framework/buttons.scss44
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss23
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss94
-rw-r--r--app/assets/stylesheets/framework/files.scss7
-rw-r--r--app/assets/stylesheets/framework/gfm.scss11
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss46
-rw-r--r--app/assets/stylesheets/framework/header.scss598
-rw-r--r--app/assets/stylesheets/framework/images.scss27
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/media_object.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss38
-rw-r--r--app/assets/stylesheets/framework/modal.scss13
-rw-r--r--app/assets/stylesheets/framework/nav.scss585
-rw-r--r--app/assets/stylesheets/framework/new-nav.scss (renamed from lib/ci/assets/.gitkeep)0
-rw-r--r--app/assets/stylesheets/framework/new-sidebar.scss510
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss376
-rw-r--r--app/assets/stylesheets/framework/selects.scss145
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/tooltips.scss7
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss106
-rw-r--r--app/assets/stylesheets/new_nav.scss468
-rw-r--r--app/assets/stylesheets/new_sidebar.scss499
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss27
-rw-r--r--app/assets/stylesheets/pages/clusters.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss8
-rw-r--r--app/assets/stylesheets/pages/convdev_index.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss207
-rw-r--r--app/assets/stylesheets/pages/environments.scss19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss42
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/note_form.scss45
-rw-r--r--app/assets/stylesheets/pages/notes.scss48
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/profile.scss17
-rw-r--r--app/assets/stylesheets/pages/projects.scss92
-rw-r--r--app/assets/stylesheets/pages/repo.scss28
-rw-r--r--app/assets/stylesheets/pages/search.scss89
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss6
-rw-r--r--app/assets/stylesheets/pages/tree.scss8
-rw-r--r--app/assets/stylesheets/test.scss6
-rw-r--r--app/controllers/admin/application_controller.rb14
-rw-r--r--app/controllers/admin/applications_controller.rb10
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb5
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb6
-rw-r--r--app/controllers/application_controller.rb22
-rw-r--r--app/controllers/autocomplete_controller.rb47
-rw-r--r--app/controllers/boards/issues_controller.rb15
-rw-r--r--app/controllers/ci/lints_controller.rb4
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb45
-rw-r--r--app/controllers/concerns/notes_actions.rb13
-rw-r--r--app/controllers/confirmations_controller.rb14
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb30
-rw-r--r--app/controllers/google_api/authorizations_controller.rb29
-rw-r--r--app/controllers/help_controller.rb10
-rw-r--r--app/controllers/oauth/applications_controller.rb10
-rw-r--r--app/controllers/profiles/avatars_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb29
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb6
-rw-r--r--app/controllers/profiles/keys_controller.rb12
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/profiles/passwords_controller.rb6
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb4
-rw-r--r--app/controllers/profiles/preferences_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb6
-rw-r--r--app/controllers/profiles_controller.rb10
-rw-r--r--app/controllers/projects/artifacts_controller.rb18
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb12
-rw-r--r--app/controllers/projects/clusters_controller.rb136
-rw-r--r--app/controllers/projects/commit_controller.rb7
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb8
-rw-r--r--app/controllers/projects/git_http_client_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb33
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/lfs_api_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb8
-rw-r--r--app/controllers/projects/network_controller.rb23
-rw-r--r--app/controllers/projects/notes_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb8
-rw-r--r--app/controllers/projects/refs_controller.rb17
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb21
-rw-r--r--app/controllers/projects/registry/tags_controller.rb27
-rw-r--r--app/controllers/projects/tree_controller.rb7
-rw-r--r--app/controllers/projects/uploads_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb16
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb33
-rw-r--r--app/controllers/root_controller.rb5
-rw-r--r--app/controllers/sessions_controller.rb55
-rw-r--r--app/finders/autocomplete_users_finder.rb60
-rw-r--r--app/finders/concerns/custom_attributes_filter.rb20
-rw-r--r--app/finders/fork_projects_finder.rb6
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb24
-rw-r--r--app/finders/merge_request_target_project_finder.rb18
-rw-r--r--app/finders/users_finder.rb2
-rw-r--r--app/helpers/appearances_helper.rb6
-rw-r--r--app/helpers/auto_devops_helper.rb26
-rw-r--r--app/helpers/avatars_helper.rb25
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/breadcrumbs_helper.rb8
-rw-r--r--app/helpers/builds_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb12
-rw-r--r--app/helpers/diff_helper.rb16
-rw-r--r--app/helpers/events_helper.rb20
-rw-r--r--app/helpers/graph_helper.rb3
-rw-r--r--app/helpers/groups_helper.rb8
-rw-r--r--app/helpers/icons_helper.rb12
-rw-r--r--app/helpers/issuables_helper.rb22
-rw-r--r--app/helpers/merge_requests_helper.rb3
-rw-r--r--app/helpers/milestones_helper.rb6
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/numbers_helper.rb11
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb24
-rw-r--r--app/helpers/sorting_helper.rb306
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/helpers/system_note_helper.rb44
-rw-r--r--app/helpers/tab_helper.rb4
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb6
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb2
-rw-r--r--app/models/ci/artifact_blob.rb26
-rw-r--r--app/models/ci/build.rb13
-rw-r--r--app/models/ci/build_trace_section.rb11
-rw-r--r--app/models/ci/build_trace_section_name.rb11
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/pipeline.rb33
-rw-r--r--app/models/ci/pipeline_schedule.rb2
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_project.rb2
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/commit.rb11
-rw-r--r--app/models/concerns/cache_markdown_field.rb14
-rw-r--r--app/models/concerns/discussion_on_diff.rb4
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb24
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/repository_mirroring.rb25
-rw-r--r--app/models/concerns/routable.rb6
-rw-r--r--app/models/concerns/sortable.rb15
-rw-r--r--app/models/concerns/token_authenticatable.rb4
-rw-r--r--app/models/deploy_key.rb6
-rw-r--r--app/models/diff_discussion.rb2
-rw-r--r--app/models/diff_note.rb14
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/email.rb12
-rw-r--r--app/models/environment.rb14
-rw-r--r--app/models/event.rb42
-rw-r--r--app/models/fork_network.rb15
-rw-r--r--app/models/fork_network_member.rb7
-rw-r--r--app/models/gcp/cluster.rb113
-rw-r--r--app/models/gpg_key.rb23
-rw-r--r--app/models/gpg_key_subkey.rb22
-rw-r--r--app/models/gpg_signature.rb36
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/key.rb13
-rw-r--r--app/models/label.rb7
-rw-r--r--app/models/legacy_diff_discussion.rb8
-rw-r--r--app/models/merge_request.rb51
-rw-r--r--app/models/merge_request_diff.rb1
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/models/namespace.rb13
-rw-r--r--app/models/network/graph.rb7
-rw-r--r--app/models/note.rb16
-rw-r--r--app/models/personal_access_token.rb8
-rw-r--r--app/models/project.rb155
-rw-r--r--app/models/project_auto_devops.rb7
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_services/chat_message/base_message.rb10
-rw-r--r--app/models/project_services/chat_message/issue_message.rb6
-rw-r--r--app/models/project_services/chat_message/merge_message.rb4
-rw-r--r--app/models/project_services/chat_message/note_message.rb4
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb6
-rw-r--r--app/models/project_services/chat_message/push_message.rb8
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/project_wiki.rb59
-rw-r--r--app/models/push_event.rb44
-rw-r--r--app/models/repository.rb211
-rw-r--r--app/models/sent_notification.rb6
-rw-r--r--app/models/storage/hashed_project.rb1
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/user.rb108
-rw-r--r--app/models/user_custom_attribute.rb6
-rw-r--r--app/models/wiki_page.rb10
-rw-r--r--app/policies/gcp/cluster_policy.rb12
-rw-r--r--app/policies/global_policy.rb11
-rw-r--r--app/policies/group_policy.rb3
-rw-r--r--app/policies/issuable_policy.rb12
-rw-r--r--app/policies/namespace_policy.rb4
-rw-r--r--app/policies/note_policy.rb2
-rw-r--r--app/policies/project_policy.rb4
-rw-r--r--app/presenters/ci/pipeline_presenter.rb11
-rw-r--r--app/presenters/gcp/cluster_presenter.rb9
-rw-r--r--app/presenters/merge_request_presenter.rb2
-rw-r--r--app/serializers/build_details_entity.rb4
-rw-r--r--app/serializers/cluster_entity.rb6
-rw-r--r--app/serializers/cluster_serializer.rb7
-rw-r--r--app/serializers/commit_entity.rb2
-rw-r--r--app/serializers/container_repositories_serializer.rb3
-rw-r--r--app/serializers/container_repository_entity.rb25
-rw-r--r--app/serializers/container_tag_entity.rb23
-rw-r--r--app/serializers/container_tags_serializer.rb17
-rw-r--r--app/serializers/environment_entity.rb4
-rw-r--r--app/serializers/environment_serializer.rb6
-rw-r--r--app/serializers/issue_entity.rb3
-rw-r--r--app/serializers/merge_request_entity.rb8
-rw-r--r--app/serializers/pipeline_entity.rb8
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb150
-rw-r--r--app/services/ci/extract_sections_from_build_trace_service.rb30
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb72
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/pipeline_trigger_service.rb2
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/commits/change_service.rb6
-rw-r--r--app/services/delete_merged_branches_service.rb19
-rw-r--r--app/services/deploy_keys/create_service.rb7
-rw-r--r--app/services/emails/base_service.rb6
-rw-r--r--app/services/emails/confirm_service.rb7
-rw-r--r--app/services/emails/create_service.rb4
-rw-r--r--app/services/emails/destroy_service.rb6
-rw-r--r--app/services/event_create_service.rb13
-rw-r--r--app/services/gpg_keys/create_service.rb9
-rw-r--r--app/services/issuable_base_service.rb23
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/keys/base_service.rb13
-rw-r--r--app/services/keys/create_service.rb9
-rw-r--r--app/services/keys/last_used_service.rb35
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/create_service.rb5
-rw-r--r--app/services/merge_requests/ff_merge_service.rb24
-rw-r--r--app/services/merge_requests/merge_service.rb34
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/notes/create_service.rb8
-rw-r--r--app/services/notification_service.rb7
-rw-r--r--app/services/projects/count_service.rb7
-rw-r--r--app/services/projects/destroy_service.rb7
-rw-r--r--app/services/projects/fork_service.rb20
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb68
-rw-r--r--app/services/projects/unlink_fork_service.rb1
-rw-r--r--app/services/projects/update_service.rb5
-rw-r--r--app/services/system_note_service.rb7
-rw-r--r--app/services/tags/create_service.rb2
-rw-r--r--app/services/todo_service.rb9
-rw-r--r--app/services/users/activity_service.rb2
-rw-r--r--app/services/users/last_push_event_service.rb83
-rw-r--r--app/services/users/update_service.rb19
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb2
-rw-r--r--app/views/admin/appearances/_form.html.haml4
-rw-r--r--app/views/admin/background_jobs/show.html.haml1
-rw-r--r--app/views/admin/cohorts/index.html.haml1
-rw-r--r--app/views/admin/conversational_development_index/show.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml37
-rw-r--r--app/views/admin/dashboard/index.html.haml14
-rw-r--r--app/views/admin/groups/index.html.haml1
-rw-r--r--app/views/admin/health_check/show.html.haml1
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/admin/jobs/index.html.haml3
-rw-r--r--app/views/admin/logs/show.html.haml1
-rw-r--r--app/views/admin/monitoring/_head.html.haml25
-rw-r--r--app/views/admin/projects/index.html.haml3
-rw-r--r--app/views/admin/requests_profiles/index.html.haml1
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/admin/system_info/show.html.haml1
-rw-r--r--app/views/admin/users/index.html.haml1
-rw-r--r--app/views/dashboard/projects/_nav.html.haml6
-rw-r--r--app/views/dashboard/projects/index.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml11
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_account.html.haml16
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_account.text.erb14
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml8
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.text.erb7
-rw-r--r--app/views/devise/mailer/confirmation_instructions.html.haml16
-rw-r--r--app/views/devise/mailer/confirmation_instructions.text.erb10
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml8
-rw-r--r--app/views/discussions/_diff_discussion.html.haml16
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml31
-rw-r--r--app/views/discussions/_new_issue_for_all_discussions.html.haml10
-rw-r--r--app/views/discussions/_new_issue_for_discussion.html.haml10
-rw-r--r--app/views/discussions/_notes.html.haml19
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/feature_highlight/_issue_boards.svg98
-rw-r--r--app/views/groups/_head.html.haml17
-rw-r--r--app/views/groups/_head_issues.html.haml19
-rw-r--r--app/views/groups/_settings_head.html.haml19
-rw-r--r--app/views/groups/activity.html.haml1
-rw-r--r--app/views/groups/edit.html.haml1
-rw-r--r--app/views/groups/issues.html.haml1
-rw-r--r--app/views/groups/labels/index.html.haml3
-rw-r--r--app/views/groups/milestones/_header_title.html.haml3
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/groups/projects.html.haml1
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml1
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/groups/subgroups.html.haml1
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/layouts/_bootlint.haml5
-rw-r--r--app/views/layouts/_head.html.haml7
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml22
-rw-r--r--app/views/layouts/header/_new_dropdown.haml4
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml6
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml61
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml22
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml50
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml66
-rw-r--r--app/views/notify/new_email_email.html.haml10
-rw-r--r--app/views/notify/new_email_email.text.erb7
-rw-r--r--app/views/profiles/accounts/show.html.haml24
-rw-r--r--app/views/profiles/emails/index.html.haml16
-rw-r--r--app/views/profiles/gpg_keys/_email_with_badge.html.haml8
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml9
-rw-r--r--app/views/profiles/preferences/show.html.haml6
-rw-r--r--app/views/projects/_head.html.haml17
-rw-r--r--app/views/projects/_home_panel.html.haml14
-rw-r--r--app/views/projects/_md_preview.html.haml7
-rw-r--r--app/views/projects/_merge_request_fast_forward_settings.html.haml13
-rw-r--r--app/views/projects/_merge_request_rebase_settings.html.haml13
-rw-r--r--app/views/projects/_merge_request_settings.html.haml15
-rw-r--r--app/views/projects/activity.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml20
-rw-r--r--app/views/projects/artifacts/browse.html.haml3
-rw-r--r--app/views/projects/artifacts/file.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml1
-rw-r--r--app/views/projects/blob/diff.html.haml31
-rw-r--r--app/views/projects/blob/edit.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml1
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml36
-rw-r--r--app/views/projects/branches/_delete_protected_modal.html.haml37
-rw-r--r--app/views/projects/branches/index.html.haml25
-rw-r--r--app/views/projects/buttons/_download.html.haml33
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml8
-rw-r--r--app/views/projects/buttons/_fork.html.haml9
-rw-r--r--app/views/projects/clusters/_form.html.haml37
-rw-r--r--app/views/projects/clusters/_header.html.haml14
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml7
-rw-r--r--app/views/projects/clusters/login.html.haml16
-rw-r--r--app/views/projects/clusters/new.html.haml9
-rw-r--r--app/views/projects/clusters/show.html.haml70
-rw-r--r--app/views/projects/commit/_pipelines_list.haml2
-rw-r--r--app/views/projects/commit/show.html.haml1
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml36
-rw-r--r--app/views/projects/commits/show.html.haml3
-rw-r--r--app/views/projects/compare/_form.html.haml20
-rw-r--r--app/views/projects/compare/index.html.haml19
-rw-r--r--app/views/projects/compare/show.html.haml1
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml17
-rw-r--r--app/views/projects/diffs/_file.html.haml6
-rw-r--r--app/views/projects/diffs/_image_diff_frame.html.haml5
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml16
-rw-r--r--app/views/projects/diffs/_replaced_image_diff.html.haml61
-rw-r--r--app/views/projects/diffs/_single_image_diff.html.haml16
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml72
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml3
-rw-r--r--app/views/projects/environments/edit.html.haml1
-rw-r--r--app/views/projects/environments/folder.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/environments/metrics.html.haml5
-rw-r--r--app/views/projects/environments/new.html.haml1
-rw-r--r--app/views/projects/environments/show.html.haml1
-rw-r--r--app/views/projects/environments/terminal.html.haml1
-rw-r--r--app/views/projects/find_file/show.html.haml1
-rw-r--r--app/views/projects/forks/new.html.haml74
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/hook_logs/show.html.haml2
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/issues/_head.html.haml33
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml4
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml4
-rw-r--r--app/views/projects/issues/edit.html.haml7
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/jobs/index.html.haml3
-rw-r--r--app/views/projects/jobs/show.html.haml1
-rw-r--r--app/views/projects/labels/edit.html.haml1
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/labels/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/_head.html.haml21
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/show.html.haml7
-rw-r--r--app/views/projects/milestones/edit.html.haml1
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/milestones/new.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml1
-rw-r--r--app/views/projects/network/show.html.haml1
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/notes/_actions.html.haml5
-rw-r--r--app/views/projects/pages/show.html.haml1
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml34
-rw-r--r--app/views/projects/pipelines/charts.html.haml1
-rw-r--r--app/views/projects/pipelines/index.html.haml6
-rw-r--r--app/views/projects/pipelines/show.html.haml1
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml25
-rw-r--r--app/views/projects/registry/repositories/_image.html.haml32
-rw-r--r--app/views/projects/registry/repositories/index.html.haml93
-rw-r--r--app/views/projects/releases/edit.html.haml1
-rw-r--r--app/views/projects/runners/_form.html.haml4
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/projects/services/edit.html.haml1
-rw-r--r--app/views/projects/settings/_head.html.haml30
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml2
-rw-r--r--app/views/projects/settings/integrations/show.html.haml1
-rw-r--r--app/views/projects/settings/members/show.html.haml1
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml7
-rw-r--r--app/views/projects/tags/index.html.haml1
-rw-r--r--app/views/projects/tags/show.html.haml1
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml8
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/projects/tree/show.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml22
-rw-r--r--app/views/projects/wikis/_main_links.html.haml6
-rw-r--r--app/views/projects/wikis/_new.html.haml11
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml4
-rw-r--r--app/views/projects/wikis/edit.html.haml19
-rw-r--r--app/views/projects/wikis/empty.html.haml6
-rw-r--r--app/views/projects/wikis/git_access.html.haml14
-rw-r--r--app/views/projects/wikis/history.html.haml18
-rw-r--r--app/views/projects/wikis/pages.html.haml8
-rw-r--r--app/views/projects/wikis/show.html.haml14
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml10
-rw-r--r--app/views/shared/_email_with_badge.html.haml8
-rw-r--r--app/views/shared/_group_form.html.haml8
-rw-r--r--app/views/shared/_mr_head.html.haml4
-rw-r--r--app/views/shared/_nav_scroll.html.haml4
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml4
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml6
-rw-r--r--app/views/shared/_sort_dropdown.html.haml42
-rw-r--r--app/views/shared/_target_switcher.html.haml20
-rw-r--r--app/views/shared/_user_callout.html.haml13
-rw-r--r--app/views/shared/boards/_show.html.haml25
-rw-r--r--app/views/shared/boards/components/_board.html.haml4
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml3
-rw-r--r--app/views/shared/empty_states/icons/_issues.svg1
-rw-r--r--app/views/shared/empty_states/icons/_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_merge_requests.svg1
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_empty.svg1
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_failed.svg1
-rw-r--r--app/views/shared/empty_states/icons/_priority_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_todos_all_done.svg1
-rw-r--r--app/views/shared/empty_states/icons/_todos_empty.svg110
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_canceled.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_created.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_failed.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_manual.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_pending.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_running.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_skipped.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_success.svg0
-rw-r--r--[-rwxr-xr-x]app/views/shared/icons/_icon_status_warning.svg0
-rw-r--r--app/views/shared/icons/_key_2.svg1
-rw-r--r--app/views/shared/icons/_thumbs_up.svg1
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml4
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--app/views/shared/issuable/_participants.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml10
-rw-r--r--app/views/shared/issuable/_user_dropdown_item.html.haml2
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_form.html.haml35
-rw-r--r--app/views/shared/notes/_note.html.haml15
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml14
-rw-r--r--app/views/u2f/_register.html.haml12
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/workers/build_finished_worker.rb1
-rw-r--r--app/workers/build_trace_sections_worker.rb8
-rw-r--r--app/workers/cluster_provision_worker.rb10
-rw-r--r--app/workers/concerns/cluster_queue.rb10
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb11
-rw-r--r--app/workers/storage_migrator_worker.rb30
-rw-r--r--app/workers/use_key_worker.rb13
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb27
-rw-r--r--changelogs/unreleased/12673-fix_v3_project_hooks_build_events4
-rw-r--r--changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md4
-rw-r--r--changelogs/unreleased/12968-generalize-profile-updates.yml4
-rw-r--r--changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml6
-rw-r--r--changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml5
-rw-r--r--changelogs/unreleased/13711-allow-same-period-housekeeping.yml6
-rw-r--r--changelogs/unreleased/14395-upgrade-gitlab-markup.yml5
-rw-r--r--changelogs/unreleased/14553-missing-space-in-log-msg.yml5
-rw-r--r--changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml5
-rw-r--r--changelogs/unreleased/18308-escape-characters.yml5
-rw-r--r--changelogs/unreleased/18608-lock-issues.yml4
-rw-r--r--changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml5
-rw-r--r--changelogs/unreleased/20049-projects-api-forks.yml5
-rw-r--r--changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml5
-rw-r--r--changelogs/unreleased/21331-improve-confusing-compare-page.yml5
-rw-r--r--changelogs/unreleased/21949-add-type-to-changelog.yml4
-rw-r--r--changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email4
-rw-r--r--changelogs/unreleased/23079-remove-default-scope-in-sortable.yml5
-rw-r--r--changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml5
-rw-r--r--changelogs/unreleased/24121_extract_yet_another_users_finder.yml5
-rw-r--r--changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml5
-rw-r--r--changelogs/unreleased/26890-fix-default-branches-sorting.yml5
-rw-r--r--changelogs/unreleased/26908-make-timelogs-use-foreign-keys4
-rw-r--r--changelogs/unreleased/28202_decrease_abc_threshold_step3.yml5
-rw-r--r--changelogs/unreleased/28283-uuid-storage.yml4
-rw-r--r--changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml4
-rw-r--r--changelogs/unreleased/28938-password-change-workflow-for-admins.yml5
-rw-r--r--changelogs/unreleased/29811-fix-line-number-alignment.yml4
-rw-r--r--changelogs/unreleased/30162-retire-koding-integration.yml4
-rw-r--r--changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml6
-rw-r--r--changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml5
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml5
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml5
-rw-r--r--changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml5
-rw-r--r--changelogs/unreleased/31470-fix-api-files-raw.yml5
-rw-r--r--changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml5
-rw-r--r--changelogs/unreleased/32340-correct-jobs-api-documentation4
-rw-r--r--changelogs/unreleased/32665-refactor-project-visibility-settings.yml5
-rw-r--r--changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml5
-rw-r--r--changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml5
-rw-r--r--changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml4
-rw-r--r--changelogs/unreleased/34102-online-view-of-artifacts-fe.yml5
-rw-r--r--changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml5
-rw-r--r--changelogs/unreleased/34261-move-move-to-sidebar.yml5
-rw-r--r--changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml5
-rw-r--r--changelogs/unreleased/34371-cycle-analitcs-global.yml5
-rw-r--r--changelogs/unreleased/34371-pipeline-schedule-vue-files.yml6
-rw-r--r--changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml4
-rw-r--r--changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml5
-rw-r--r--changelogs/unreleased/34510-board-issues-sql-speedup.yml5
-rw-r--r--changelogs/unreleased/34643-fix-project-path-slugify.yml4
-rw-r--r--changelogs/unreleased/34945-readme-div-id.yml5
-rw-r--r--changelogs/unreleased/34990-top-buttons-misaligned.yml5
-rw-r--r--changelogs/unreleased/35010-projects-nav-dropdown.yml5
-rw-r--r--changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml5
-rw-r--r--changelogs/unreleased/35048-empty-badges.yml5
-rw-r--r--changelogs/unreleased/35161_first_time_contributor_badge.yml4
-rw-r--r--changelogs/unreleased/3523-i18n-autodevops.yml5
-rw-r--r--changelogs/unreleased/35290_allow_public_project_apis.yml4
-rw-r--r--changelogs/unreleased/35343-inherit-milestones-and-labels.yml5
-rw-r--r--changelogs/unreleased/35441-fix-division-by-zero.yml5
-rw-r--r--changelogs/unreleased/35686-unescape-wiki-title.yml5
-rw-r--r--changelogs/unreleased/35721-auth-style-confirmation.yml5
-rw-r--r--changelogs/unreleased/35793_fix_predicate_names.yml5
-rw-r--r--changelogs/unreleased/35811-copy-link-note.yml5
-rw-r--r--changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml5
-rw-r--r--changelogs/unreleased/35917_create_services_for_keys.yml4
-rw-r--r--changelogs/unreleased/35942-api-binary-encoding.yaml3
-rw-r--r--changelogs/unreleased/35994-archived-projects-only.yml5
-rw-r--r--changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml4
-rw-r--r--changelogs/unreleased/36041-notification-title.yml4
-rw-r--r--changelogs/unreleased/36087-users-cannot-delete-their-account.yml5
-rw-r--r--changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml4
-rw-r--r--changelogs/unreleased/36119-issuable-workers.yml4
-rw-r--r--changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml5
-rw-r--r--changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml6
-rw-r--r--changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml5
-rw-r--r--changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml5
-rw-r--r--changelogs/unreleased/36385-pipeline-graph-dropdown.yml5
-rw-r--r--changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml5
-rw-r--r--changelogs/unreleased/36611-error-in-getcomposer-link.yml5
-rw-r--r--changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml6
-rw-r--r--changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml5
-rw-r--r--changelogs/unreleased/36670-remove-edit-form.yml5
-rw-r--r--changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml5
-rw-r--r--changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml5
-rw-r--r--changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml5
-rw-r--r--changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml5
-rw-r--r--changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml5
-rw-r--r--changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml5
-rw-r--r--changelogs/unreleased/36860-migrate-issues-author.yml5
-rw-r--r--changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml5
-rw-r--r--changelogs/unreleased/36884-gitaly-admin-version.yml5
-rw-r--r--changelogs/unreleased/36917-branch-tooltip.yml5
-rw-r--r--changelogs/unreleased/36937-fix-invite-by-email-text.yml5
-rw-r--r--changelogs/unreleased/36939-fix-find-blobs-by-path.yml5
-rw-r--r--changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml5
-rw-r--r--changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml5
-rw-r--r--changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml5
-rw-r--r--changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml4
-rw-r--r--changelogs/unreleased/37104-fix-graph-date-format.yml5
-rw-r--r--changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml5
-rw-r--r--changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml5
-rw-r--r--changelogs/unreleased/37158-autodevops-banner.yml5
-rw-r--r--changelogs/unreleased/37179-dashboard-project-dropdown.yml5
-rw-r--r--changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml5
-rw-r--r--changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml5
-rw-r--r--changelogs/unreleased/37220-es-modules.yml5
-rw-r--r--changelogs/unreleased/37229-mr-widget-status-icon.yml5
-rw-r--r--changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml5
-rw-r--r--changelogs/unreleased/37331-button-MR-widget.yml5
-rw-r--r--changelogs/unreleased/37335-counter-active-state.yml5
-rw-r--r--changelogs/unreleased/37368-blob-viewer-on-mobile.yml5
-rw-r--r--changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml5
-rw-r--r--changelogs/unreleased/37406-success-status-icon.yml5
-rw-r--r--changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml5
-rw-r--r--changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml5
-rw-r--r--changelogs/unreleased/37552-replace-js-true-with-js.yml5
-rw-r--r--changelogs/unreleased/37970-ci-sections-tracking.yml5
-rw-r--r--changelogs/unreleased/37970-timestamped-ci.yml5
-rw-r--r--changelogs/unreleased/37999-fix-circuit-breaker.yml5
-rw-r--r--changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml6
-rw-r--r--changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml5
-rw-r--r--changelogs/unreleased/38052-use-simple-api-for-projects.yml5
-rw-r--r--changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml5
-rw-r--r--changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml5
-rw-r--r--changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml6
-rw-r--r--changelogs/unreleased/38234-reserve-refs-replace.yml5
-rw-r--r--changelogs/unreleased/38389-allow-merge-without-success.yml6
-rw-r--r--changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml5
-rw-r--r--changelogs/unreleased/38432-fix-notes-type-for-import.yml6
-rw-r--r--changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml5
-rw-r--r--changelogs/unreleased/38534-minigraph.yml5
-rw-r--r--changelogs/unreleased/38571-fix-exception-in-raven-report.yml6
-rw-r--r--changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml5
-rw-r--r--changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml5
-rw-r--r--changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/38775-scrollable-tabs-on-admin.yml5
-rw-r--r--changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml5
-rw-r--r--changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml5
-rw-r--r--changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml5
-rw-r--r--changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml5
-rw-r--r--changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml5
-rw-r--r--changelogs/unreleased/add-filter-by-my-reaction.yml4
-rw-r--r--changelogs/unreleased/add-labels-template-index.yml5
-rw-r--r--changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml4
-rw-r--r--changelogs/unreleased/add-view-replaced-file-link.yml5
-rw-r--r--changelogs/unreleased/add_closed_at_attribute.yml5
-rw-r--r--changelogs/unreleased/add_message_to_the_404_page.yml5
-rw-r--r--changelogs/unreleased/add_quick_submission_on_user_settings_page.yml5
-rw-r--r--changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml5
-rw-r--r--changelogs/unreleased/additional-time-series-charts.yml5
-rw-r--r--changelogs/unreleased/adjusting-tooltips.yml5
-rw-r--r--changelogs/unreleased/api-delete-respect-headers.yml5
-rw-r--r--changelogs/unreleased/api-gpg-key-management.yml5
-rw-r--r--changelogs/unreleased/api_branches_head.yml5
-rw-r--r--changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml5
-rw-r--r--changelogs/unreleased/breadcrumb-item-links.yml5
-rw-r--r--changelogs/unreleased/breadcrumbs-line-height-padding.yml5
-rw-r--r--changelogs/unreleased/bugfix-graph-friendly-notes-number.yml5
-rw-r--r--changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml4
-rw-r--r--changelogs/unreleased/bvl-fork-network-schema.yml5
-rw-r--r--changelogs/unreleased/bvl-improve-bare-project-import.yml6
-rw-r--r--changelogs/unreleased/bvl-validate-po-files.yml4
-rw-r--r--changelogs/unreleased/cache-issue-and-mr-counts.yml5
-rw-r--r--changelogs/unreleased/change-dashed-border-button-color.yml5
-rw-r--r--changelogs/unreleased/check-trigger-permissions.yml5
-rw-r--r--changelogs/unreleased/close-issue-by-implements.yml5
-rw-r--r--changelogs/unreleased/collapsable-pipeline-settings.yml5
-rw-r--r--changelogs/unreleased/commit-row-avatar-align-top.yml5
-rw-r--r--changelogs/unreleased/consistent-tooltip-direction-on-commits.yml5
-rw-r--r--changelogs/unreleased/content-title-link-hover-bg.yml5
-rw-r--r--changelogs/unreleased/declarative-policy-optimisations.yml5
-rw-r--r--changelogs/unreleased/disable-project-export.yml4
-rw-r--r--changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml6
-rw-r--r--changelogs/unreleased/dm-closing-issue-urls.yml5
-rw-r--r--changelogs/unreleased/dm-copy-parallel-diff.yml5
-rw-r--r--changelogs/unreleased/dm-pat-revoke.yml5
-rw-r--r--changelogs/unreleased/dm-simple-project-avatar-url.yml5
-rw-r--r--changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml5
-rw-r--r--changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml5
-rw-r--r--changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml5
-rw-r--r--changelogs/unreleased/docs-add-summary-about-project-archiving.yml5
-rw-r--r--changelogs/unreleased/docs-confidential-issue.yml5
-rw-r--r--changelogs/unreleased/docs-document-version-for-group-milestones-api.yml5
-rw-r--r--changelogs/unreleased/docs-fix-15669-issue-move-api.yml5
-rw-r--r--changelogs/unreleased/docs-openid-connect.yml5
-rw-r--r--changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml5
-rw-r--r--changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml5
-rw-r--r--changelogs/unreleased/ee-add-project-repository-storages-index.yml5
-rw-r--r--changelogs/unreleased/events-migration-cleanup.yml5
-rw-r--r--changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml5
-rw-r--r--changelogs/unreleased/feature-custom-attributes.yml4
-rw-r--r--changelogs/unreleased/feature-dependency-status-badge.yml5
-rw-r--r--changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml5
-rw-r--r--changelogs/unreleased/feature-gpg-verification-status.yml6
-rw-r--r--changelogs/unreleased/feature-plantuml-restructured-text.yml5
-rw-r--r--changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml5
-rw-r--r--changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml5
-rw-r--r--changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml5
-rw-r--r--changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml5
-rw-r--r--changelogs/unreleased/feature-verify_secondary_emails.yml5
-rw-r--r--changelogs/unreleased/ff_port_from_ee.yml5
-rw-r--r--changelogs/unreleased/fix-btn-alignment.yml5
-rw-r--r--changelogs/unreleased/fix-edit-merge-request-button-case.yml5
-rw-r--r--changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml5
-rw-r--r--changelogs/unreleased/fix-gpg-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/fix-image-diff-swipe-handle.yml5
-rw-r--r--changelogs/unreleased/fix-import-export-performance.yml5
-rw-r--r--changelogs/unreleased/fix-import-fork-mr.yml5
-rw-r--r--changelogs/unreleased/fix-multi-line-hook-output.yml5
-rw-r--r--changelogs/unreleased/fix-npm-security-updates.yml5
-rw-r--r--changelogs/unreleased/fix-sidebar-with-scrollbars.yml5
-rw-r--r--changelogs/unreleased/fix-stray-or-in-project-create-ui.yml5
-rw-r--r--changelogs/unreleased/fix-tooltip-width-issue-board.yml5
-rw-r--r--changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml5
-rw-r--r--changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml5
-rw-r--r--changelogs/unreleased/fix_wiki_toc_indent.yml5
-rw-r--r--changelogs/unreleased/fl-fix-ca-time-component.yml5
-rw-r--r--changelogs/unreleased/font-weight-adjusted.yml5
-rw-r--r--changelogs/unreleased/force-two-up-view.yml5
-rw-r--r--changelogs/unreleased/fuzzy-issue-search.yml5
-rw-r--r--changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml5
-rw-r--r--changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml5
-rw-r--r--changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml5
-rw-r--r--changelogs/unreleased/gitaly_feature_flag_metadata.yml5
-rw-r--r--changelogs/unreleased/gitaly_ref_exists.yml4
-rw-r--r--changelogs/unreleased/group-milestones-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/group-mr-search-bar.yml5
-rw-r--r--changelogs/unreleased/group-sort-dropdown-blank.yml5
-rw-r--r--changelogs/unreleased/hash-mr-scroll-load.yml5
-rw-r--r--changelogs/unreleased/hashed-storage-migration-path.yml5
-rw-r--r--changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml4
-rw-r--r--changelogs/unreleased/ie-event-polyfill.yml5
-rw-r--r--changelogs/unreleased/import-sources-fix.yml5
-rw-r--r--changelogs/unreleased/improve-autocomplete-user-performance.yml5
-rw-r--r--changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml6
-rw-r--r--changelogs/unreleased/improve_sorting_list.yml5
-rw-r--r--changelogs/unreleased/issue-api-my-reaction.yml5
-rw-r--r--changelogs/unreleased/issue-boards-breadcrumbs-container.yml5
-rw-r--r--changelogs/unreleased/issue_32215.yml5
-rw-r--r--changelogs/unreleased/issue_35873.yml5
-rw-r--r--changelogs/unreleased/italicized_emoji.yml5
-rw-r--r--changelogs/unreleased/jobs-sort-by-id.yml5
-rw-r--r--changelogs/unreleased/lint-changelog-yaml.yml5
-rw-r--r--changelogs/unreleased/mentions-in-comments.yml5
-rw-r--r--changelogs/unreleased/merge-request-notes-performance.yml5
-rw-r--r--changelogs/unreleased/milestone-avatar-issuable-link.yml5
-rw-r--r--changelogs/unreleased/mk-clarify-moving-namespaces.yml5
-rw-r--r--changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml5
-rw-r--r--changelogs/unreleased/mk-normalize-ldap-user-dns.yml5
-rw-r--r--changelogs/unreleased/move-action.yml4
-rw-r--r--changelogs/unreleased/mr-index-page-performance.yml5
-rw-r--r--changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml5
-rw-r--r--changelogs/unreleased/mr-widget-merged-date-tooltip.yml5
-rw-r--r--changelogs/unreleased/perf-slow-issuable.yml6
-rw-r--r--changelogs/unreleased/project-page-clearer.yml5
-rw-r--r--changelogs/unreleased/rc-fix-gh-import-branches-performance.yml5
-rw-r--r--changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml5
-rw-r--r--changelogs/unreleased/refactor-animate-js.yml5
-rw-r--r--changelogs/unreleased/refactor-monitoring-service.yml5
-rw-r--r--changelogs/unreleased/remote_user.yml4
-rw-r--r--changelogs/unreleased/remove-temporary-ci-index.yml5
-rw-r--r--changelogs/unreleased/remove-use-key-worker.yml5
-rw-r--r--changelogs/unreleased/remove_repo_prefix_from_api.yml5
-rw-r--r--changelogs/unreleased/replace_emails-feature.yml5
-rw-r--r--changelogs/unreleased/replace_group_links-feature.yml5
-rw-r--r--changelogs/unreleased/replace_milestone-feature.yml5
-rw-r--r--changelogs/unreleased/replace_profile_active_tab-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_archived-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_builds_summary-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_commits_revert-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_issues_award_emoji-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests_accept-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests_revert-feature.yml6
-rw-r--r--changelogs/unreleased/replace_project_service-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_shortcuts-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_snippets-feature.yml5
-rw-r--r--changelogs/unreleased/replace_search-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_search_code-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_star-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_user_lookup-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_wiki-feature.yml5
-rw-r--r--changelogs/unreleased/replace_team_management-feature.yml5
-rw-r--r--changelogs/unreleased/repository-name-emojis4
-rw-r--r--changelogs/unreleased/rotated_profile_image.yml5
-rw-r--r--changelogs/unreleased/rouge-2-2-0.yml5
-rw-r--r--changelogs/unreleased/rouge-2-2-1.yml5
-rw-r--r--changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml5
-rw-r--r--changelogs/unreleased/seven-days-cycle-analytics.yml5
-rw-r--r--changelogs/unreleased/sh-add-grape-logging.yml5
-rw-r--r--changelogs/unreleased/sh-bump-jira-gem.yml5
-rw-r--r--changelogs/unreleased/sh-fix-username-logging.yml5
-rw-r--r--changelogs/unreleased/sh-show-all-slack-names.yml5
-rw-r--r--changelogs/unreleased/sh-thread-safe-markdown.yml5
-rw-r--r--changelogs/unreleased/sidebar-cache-updates.yml5
-rw-r--r--changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml5
-rw-r--r--changelogs/unreleased/tag-link-size.yml5
-rw-r--r--changelogs/unreleased/tc-geo-read-only-idea.yml5
-rw-r--r--changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml5
-rw-r--r--changelogs/unreleased/uipolish-fix-2factor-warning.yml5
-rw-r--r--changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml5
-rw-r--r--changelogs/unreleased/update-pages-0-6.yml5
-rw-r--r--changelogs/unreleased/url-sanitizer-fixes.yml5
-rw-r--r--changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml5
-rw-r--r--changelogs/unreleased/valid-branch-name-dash-bug.yml5
-rw-r--r--changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml5
-rw-r--r--changelogs/unreleased/wiki_api.yml5
-rw-r--r--changelogs/unreleased/winh-delete-account-modal.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-changelog-docs.yml5
-rw-r--r--changelogs/unreleased/winh-protected-branch-modal-merged.yml5
-rw-r--r--changelogs/unreleased/winh-sprintf.yml5
-rw-r--r--changelogs/unreleased/zj-add-pipeline-source-variable.yml5
-rw-r--r--changelogs/unreleased/zj-auto-devops-table.yml5
-rw-r--r--changelogs/unreleased/zj-disable-pages-in-subgroups.yml5
-rw-r--r--changelogs/unreleased/zj-remove-ci-api-v1.yml5
-rw-r--r--changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml5
-rw-r--r--changelogs/unreleased/zj-sort-templates.yml5
-rw-r--r--changelogs/unreleased/zj-update-rails-template.yml5
-rw-r--r--changelogs/unreleased/zj-upgrade-grape.yml5
-rw-r--r--config/application.rb10
-rw-r--r--config/dependency_decisions.yml48
-rw-r--r--config/environments/test.rb2
-rw-r--r--config/gitlab.yml.example13
-rw-r--r--config/initializers/1_settings.rb27
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb2
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb14
-rw-r--r--config/initializers/grpc.rb11
-rw-r--r--config/initializers/lograge.rb7
-rw-r--r--config/initializers/postgresql_opclasses_support.rb2
-rw-r--r--config/initializers/secret_token.rb2
-rw-r--r--config/initializers/sentry.rb4
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--config/prometheus/additional_metrics.yml48
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/google_api.rb5
-rw-r--r--config/routes/profile.rb9
-rw-r--r--config/routes/project.rb12
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--config/svg.config.js48
-rw-r--r--config/webpack.config.js10
-rw-r--r--db/fixtures/development/04_project.rb4
-rw-r--r--db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb17
-rw-r--r--db/migrate/20150827121444_add_fast_forward_option_to_project.rb19
-rw-r--r--db/migrate/20160713200638_add_repository_read_only_to_projects.rb9
-rw-r--r--db/migrate/20170720122741_create_user_custom_attributes.rb17
-rw-r--r--db/migrate/20170815221154_add_discussion_locked_to_issuable.rb13
-rw-r--r--db/migrate/20170828135939_migrate_user_external_mail_data.rb2
-rw-r--r--db/migrate/20170830131015_swap_event_migration_tables.rb24
-rw-r--r--db/migrate/20170904092148_add_email_confirmation.rb33
-rw-r--r--db/migrate/20170909090114_add_email_confirmation_index.rb36
-rw-r--r--db/migrate/20170912113435_clean_stages_statuses_migration.rb26
-rw-r--r--db/migrate/20170913131410_environments_project_id_not_null.rb16
-rw-r--r--db/migrate/20170914135630_add_index_for_recent_push_events.rb40
-rw-r--r--db/migrate/20170918222253_reorganize_deployments_indexes.rb28
-rw-r--r--db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb21
-rw-r--r--db/migrate/20170919211300_remove_temporary_ci_builds_index.rb27
-rw-r--r--db/migrate/20170921115009_add_project_repository_storage_index.rb19
-rw-r--r--db/migrate/20170924094327_create_gcp_clusters.rb45
-rw-r--r--db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb39
-rw-r--r--db/migrate/20170927122209_add_partial_index_for_labels_template.rb45
-rw-r--r--db/migrate/20170927161718_create_gpg_key_subkeys.rb23
-rw-r--r--db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb33
-rw-r--r--db/migrate/20170928124105_create_fork_networks.rb28
-rw-r--r--db/migrate/20170928133643_create_fork_network_members.rb26
-rw-r--r--db/migrate/20170929080234_add_failure_reason_to_pipelines.rb9
-rw-r--r--db/migrate/20170929131201_populate_fork_networks.rb30
-rw-r--r--db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb25
-rw-r--r--db/migrate/20171006090001_create_ci_build_trace_sections.rb19
-rw-r--r--db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb15
-rw-r--r--db/migrate/20171006090100_create_ci_build_trace_section_names.rb19
-rw-r--r--db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb15
-rw-r--r--db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb2
-rw-r--r--db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb37
-rw-r--r--db/post_migrate/20170913180600_fix_projects_without_project_feature.rb33
-rw-r--r--db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb29
-rw-r--r--db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb32
-rw-r--r--db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb16
-rw-r--r--db/post_migrate/20170927112319_update_notes_type_for_import.rb16
-rw-r--r--db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb28
-rw-r--r--db/schema.rb127
-rw-r--r--doc/README.md8
-rw-r--r--doc/administration/auth/ldap.md6
-rw-r--r--doc/administration/gitaly/index.md12
-rw-r--r--doc/administration/job_artifacts.md6
-rw-r--r--doc/administration/raketasks/storage.md107
-rw-r--r--doc/administration/reply_by_email.md31
-rw-r--r--doc/administration/repository_storage_types.md69
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/commits.md6
-rw-r--r--doc/api/custom_attributes.md105
-rw-r--r--doc/api/issues.md67
-rw-r--r--doc/api/jobs.md2
-rw-r--r--doc/api/merge_requests.md20
-rw-r--r--doc/api/projects.md92
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/api/wikis.md2
-rw-r--r--doc/ci/docker/using_docker_build.md29
-rw-r--r--doc/ci/enable_or_disable_ci.md39
-rw-r--r--doc/ci/environments.md114
-rw-r--r--doc/ci/img/builds_tab.pngbin1956 -> 0 bytes
-rw-r--r--doc/ci/img/deployments_view.pngbin19923 -> 61088 bytes
-rw-r--r--doc/ci/img/environments_available.pngbin0 -> 21089 bytes
-rw-r--r--doc/ci/img/environments_available_staging.pngbin10098 -> 0 bytes
-rw-r--r--doc/ci/img/environments_dynamic_groups.pngbin45349 -> 58239 bytes
-rw-r--r--doc/ci/img/environments_link_url.pngbin12277 -> 0 bytes
-rw-r--r--doc/ci/img/environments_link_url_deployments.pngbin7490 -> 0 bytes
-rw-r--r--doc/ci/img/environments_link_url_mr.pngbin17947 -> 34361 bytes
-rw-r--r--doc/ci/img/environments_manual_action_builds.pngbin11137 -> 0 bytes
-rw-r--r--doc/ci/img/environments_manual_action_deployments.pngbin12563 -> 32748 bytes
-rw-r--r--doc/ci/img/environments_manual_action_environments.pngbin14914 -> 24191 bytes
-rw-r--r--doc/ci/img/environments_manual_action_jobs.pngbin0 -> 19919 bytes
-rw-r--r--doc/ci/img/environments_manual_action_pipelines.pngbin16243 -> 38974 bytes
-rw-r--r--doc/ci/img/environments_manual_action_single_pipeline.pngbin16576 -> 23381 bytes
-rw-r--r--doc/ci/img/environments_monitoring.pngbin243491 -> 76086 bytes
-rw-r--r--doc/ci/img/environments_mr_review_app.pngbin15366 -> 30991 bytes
-rw-r--r--doc/ci/img/environments_terminal_button_on_index.pngbin79725 -> 29162 bytes
-rw-r--r--doc/ci/img/environments_terminal_button_on_show.pngbin73210 -> 17811 bytes
-rw-r--r--doc/ci/img/environments_view.pngbin21155 -> 0 bytes
-rw-r--r--doc/ci/img/permissions_settings.pngbin39194 -> 0 bytes
-rw-r--r--doc/ci/img/prometheus_environment_detail_with_metrics.pngbin120479 -> 0 bytes
-rw-r--r--doc/ci/quick_start/README.md8
-rw-r--r--doc/ci/runners/README.md20
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/triggers/README.md6
-rw-r--r--doc/ci/variables/README.md43
-rw-r--r--doc/ci/variables/img/secret_variables.pngbin0 -> 15658 bytes
-rw-r--r--doc/ci/yaml/README.md56
-rw-r--r--doc/development/README.md9
-rw-r--r--doc/development/background_migrations.md29
-rw-r--r--doc/development/code_review.md10
-rw-r--r--doc/development/emails.md23
-rw-r--r--doc/development/fe_guide/icons.md40
-rw-r--r--doc/development/fe_guide/index.md33
-rw-r--r--doc/development/fe_guide/style_guide_js.md46
-rw-r--r--doc/development/fe_guide/vue.md2
-rw-r--r--doc/development/gitaly.md84
-rw-r--r--doc/development/i18n/externalization.md296
-rw-r--r--doc/development/i18n/img/crowdin-editor.pngbin0 -> 88701 bytes
-rw-r--r--doc/development/i18n/index.md76
-rw-r--r--doc/development/i18n/translation.md64
-rw-r--r--doc/development/i18n_guide.md298
-rw-r--r--doc/development/img/manual_build_docs.pngbin14869 -> 14867 bytes
-rw-r--r--doc/development/licensing.md3
-rw-r--r--doc/development/swapping_tables.md53
-rw-r--r--doc/development/testing.md32
-rw-r--r--doc/development/ux_guide/animation.md10
-rw-r--r--doc/development/ux_guide/basics.md22
-rw-r--r--doc/development/ux_guide/components.md50
-rw-r--r--doc/development/ux_guide/illustrations.md86
-rw-r--r--doc/development/ux_guide/img/icon-spec.pngbin0 -> 13889 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-large-horizontal.pngbin0 -> 55272 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-large-vertical.pngbin0 -> 59217 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-medium.pngbin0 -> 20994 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-small.pngbin0 -> 43536 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-border-radius.pngbin0 -> 7779 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-caps-do.pngbin0 -> 3775 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-caps-don't.pngbin0 -> 3922 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-grey.pngbin0 -> 251 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-orange.pngbin0 -> 275 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-purple.pngbin0 -> 275 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-geometric.pngbin0 -> 5057 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-palette-oragne.pngbin0 -> 10439 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-palette-purple.pngbin0 -> 10002 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-above.pngbin0 -> 68451 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-below.pngbin0 -> 63368 bytes
-rw-r--r--doc/development/ux_guide/img/skeleton-loading.gifbin0 -> 1093917 bytes
-rw-r--r--doc/development/ux_guide/index.md5
-rw-r--r--doc/development/verifying_database_capabilities.md12
-rw-r--r--doc/development/writing_documentation.md126
-rw-r--r--doc/install/database_mysql.md9
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/install/kubernetes/gitlab_chart.md15
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md57
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md4
-rw-r--r--doc/install/kubernetes/index.md65
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/integration/azure.md3
-rw-r--r--doc/raketasks/backup_restore.md14
-rw-r--r--doc/topics/authentication/index.md1
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin31851 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin89206 -> 69473 bytes
-rw-r--r--doc/topics/autodevops/index.md540
-rw-r--r--doc/topics/autodevops/quick_start_guide.md89
-rw-r--r--[-rwxr-xr-x]doc/university/training/gitlab_flow.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/index.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/additional_resources.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/agile_git.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/bisect.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/cherry_picking.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/env_setup.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/explore_gitlab.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/feature_branching.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/getting_started.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/git_add.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/git_intro.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/git_log.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/gitlab_flow.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/merge_conflicts.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/merge_requests.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/rollback_commits.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/stash.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/subtree.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/tags.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/topics/unstage.md0
-rw-r--r--[-rwxr-xr-x]doc/university/training/user_training.md0
-rw-r--r--doc/update/10.0-to-10.1.md356
-rw-r--r--doc/update/8.17-to-9.0.md2
-rw-r--r--doc/update/9.0-to-9.1.md2
-rw-r--r--doc/update/9.1-to-9.2.md2
-rw-r--r--doc/update/9.2-to-9.3.md2
-rw-r--r--doc/update/9.3-to-9.4.md2
-rw-r--r--doc/update/9.4-to-9.5.md2
-rw-r--r--doc/update/9.5-to-10.0.md356
-rw-r--r--doc/update/mysql_to_postgresql.md297
-rw-r--r--doc/update/patch_versions.md14
-rw-r--r--doc/user/admin_area/monitoring/convdev.md2
-rw-r--r--doc/user/admin_area/monitoring/health_check.md2
-rw-r--r--doc/user/admin_area/monitoring/img/convdev_index.pngbin31012 -> 116112 bytes
-rw-r--r--doc/user/discussions/img/discussion_lock_system_notes.pngbin0 -> 50200 bytes
-rw-r--r--doc/user/discussions/img/lock_form_member.pngbin0 -> 100581 bytes
-rw-r--r--doc/user/discussions/img/lock_form_non_member.pngbin0 -> 37432 bytes
-rw-r--r--doc/user/discussions/img/turn_off_lock.pngbin0 -> 31580 bytes
-rw-r--r--doc/user/discussions/img/turn_on_lock.pngbin0 -> 34839 bytes
-rw-r--r--doc/user/discussions/index.md40
-rw-r--r--doc/user/group/index.md21
-rw-r--r--doc/user/group/subgroups/index.md9
-rw-r--r--doc/user/markdown.md24
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/profile/index.md23
-rw-r--r--doc/user/project/container_registry.md22
-rw-r--r--doc/user/project/img/container_registry.pngbin0 -> 35202 bytes
-rw-r--r--doc/user/project/img/container_registry_enable.pngbin3057 -> 0 bytes
-rw-r--r--doc/user/project/img/container_registry_tab.pngbin3800 -> 0 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin51439 -> 82592 bytes
-rw-r--r--doc/user/project/img/issue_board_move_issue_card_list.pngbin74826 -> 36747 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_in_new_issue.pngbin11636 -> 0 bytes
-rw-r--r--doc/user/project/img/labels_default.pngbin32030 -> 24404 bytes
-rw-r--r--doc/user/project/img/labels_filter.pngbin31931 -> 19071 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin23969 -> 38717 bytes
-rw-r--r--doc/user/project/img/labels_new_label.pngbin16787 -> 10720 bytes
-rw-r--r--doc/user/project/img/labels_prioritize.pngbin38185 -> 24194 bytes
-rw-r--r--doc/user/project/img/project_repository_settings.pngbin35236 -> 17872 bytes
-rw-r--r--doc/user/project/import/github.md5
-rw-r--r--doc/user/project/index.md6
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.pngbin113827 -> 14407 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/integrations/img/webhook_logs.pngbin24066 -> 24066 bytes
-rw-r--r--doc/user/project/integrations/kubernetes.md134
-rw-r--r--doc/user/project/integrations/prometheus_library/cloudwatch.md5
-rw-r--r--doc/user/project/integrations/prometheus_library/haproxy.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md12
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md7
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md31
-rw-r--r--doc/user/project/issue_board.md6
-rw-r--r--doc/user/project/issues/deleting_issues.md11
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/button_close_issue.pngbin15508 -> 12274 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/closing_and_related_issues.pngbin6395 -> 6395 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_create.pngbin8185 -> 8185 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_search_guest.pngbin8593 -> 8593 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_search_master.pngbin13228 -> 13228 bytes
-rw-r--r--doc/user/project/issues/img/delete_issue.pngbin0 -> 49894 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/due_dates_create.pngbin6992 -> 6992 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/due_dates_edit_sidebar.pngbin1700 -> 1700 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/due_dates_issues_index_page.pngbin19302 -> 19302 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/due_dates_todos.pngbin4799 -> 4799 bytes
-rw-r--r--doc/user/project/issues/img/group_issues_list_view.pngbin265130 -> 127781 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/issue_board.pngbin58645 -> 56253 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/issue_template.pngbin28061 -> 25022 bytes
-rw-r--r--doc/user/project/issues/img/issues_main_view.pngbin73751 -> 72540 bytes
-rw-r--r--doc/user/project/issues/img/issues_main_view_numbered.jpgbin103249 -> 205803 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/mention_in_issue.pngbin3738 -> 3738 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/mention_in_merge_request.pngbin3944 -> 3944 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/merge_request_closes_issue.pngbin19423 -> 19423 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/new_issue.pngbin31727 -> 28734 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/new_issue_from_issue_board.pngbin137175 -> 57427 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/new_issue_from_open_issue.pngbin20628 -> 13346 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/new_issue_from_projects_dashboard.pngbin29865 -> 23685 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/new_issue_from_tracker_list.pngbin24345 -> 19632 bytes
-rw-r--r--doc/user/project/issues/img/project_issues_list_view.pngbin309131 -> 196071 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/sidebar_confidential_issue.pngbin10210 -> 10210 bytes
-rw-r--r--doc/user/project/issues/img/sidebar_move_issue.pngbin54511 -> 50132 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/sidebar_not_confidential_issue.pngbin8163 -> 8163 bytes
-rw-r--r--doc/user/project/issues/index.md4
-rw-r--r--doc/user/project/issues/issues_functionalities.md3
-rw-r--r--doc/user/project/labels.md45
-rw-r--r--doc/user/project/merge_requests/cherry_pick_changes.md31
-rw-r--r--doc/user/project/merge_requests/fast_forward_merge.md35
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit.pngbin141744 -> 13604 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.pngbin111488 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr.pngbin93870 -> 16494 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.pngbin86650 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/commit_compare.pngbin33385 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_mr.pngbin0 -> 21380 bytes
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_rebase_locally.pngbin0 -> 21013 bytes
-rw-r--r--doc/user/project/merge_requests/img/group_merge_requests_list_view.pngbin283066 -> 89620 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request.pngbin0 -> 67228 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.pngbin60346 -> 22791 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit_modal.pngbin88824 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr_modal.pngbin93536 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions.pngbin55703 -> 23629 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_compare.pngbin24886 -> 17228 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_dropdown.pngbin21547 -> 13887 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_blocked_accept_button.pngbin18606 -> 8071 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_mark_as_wip.pngbin11396 -> 17081 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_unmark_as_wip.pngbin8565 -> 18585 bytes
-rw-r--r--doc/user/project/merge_requests/index.md24
-rw-r--r--doc/user/project/merge_requests/revert_changes.md44
-rw-r--r--doc/user/project/pages/getting_started_part_one.md4
-rw-r--r--doc/user/project/pages/getting_started_part_three.md47
-rw-r--r--doc/user/project/pages/introduction.md17
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_browser.pngbin3771 -> 3944 bytes
-rw-r--r--doc/user/project/pipelines/job_artifacts.md8
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--doc/user/project/repository/branches/index.md27
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md26
-rw-r--r--[-rwxr-xr-x]doc/user/project/repository/img/compare_branches.pngbin35999 -> 206831 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/repository/img/contributors_graph.pngbin31670 -> 31670 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/repository/img/repo_graph.pngbin52317 -> 52317 bytes
-rw-r--r--doc/user/project/repository/index.md5
-rw-r--r--doc/user/project/settings/img/general_settings.pngbin0 -> 35871 bytes
-rw-r--r--doc/user/project/settings/img/merge_requests_settings.pngbin0 -> 52029 bytes
-rw-r--r--doc/user/project/settings/img/sharing_and_permissions_settings.pngbin0 -> 143341 bytes
-rw-r--r--doc/user/project/settings/index.md52
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/issues_any_assignee.pngbin90455 -> 90455 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/issues_assigned_to_you.pngbin49079 -> 49079 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/issues_author.pngbin55217 -> 55217 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/issues_mrs_shortcut.pngbin34115 -> 34115 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/left_menu_bar.pngbin37433 -> 37433 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/project_search.pngbin41900 -> 41900 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/search_issues_board.pngbin82113 -> 82113 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/search/img/sort_projects.pngbin59495 -> 59495 bytes
-rw-r--r--doc/user/search/index.md6
-rw-r--r--doc/workflow/README.md1
-rw-r--r--features/profile/active_tab.feature29
-rw-r--r--features/profile/emails.feature26
-rw-r--r--features/project/archived.feature30
-rw-r--r--features/project/builds/summary.feature30
-rw-r--r--features/project/commits/revert.feature31
-rw-r--r--features/project/ff_merge_requests.feature24
-rw-r--r--features/project/group_links.feature16
-rw-r--r--features/project/issues/award_emoji.feature45
-rw-r--r--features/project/issues/issues.feature18
-rw-r--r--features/project/merge_requests.feature326
-rw-r--r--features/project/merge_requests/accept.feature28
-rw-r--r--features/project/merge_requests/revert.feature29
-rw-r--r--features/project/milestone.feature16
-rw-r--r--features/project/service.feature87
-rw-r--r--features/project/shortcuts.feature63
-rw-r--r--features/project/snippets.feature35
-rw-r--r--features/project/team_management.feature26
-rw-r--r--features/project/wiki.feature101
-rw-r--r--features/search.feature100
-rw-r--r--features/steps/profile/active_tab.rb25
-rw-r--r--features/steps/profile/emails.rb48
-rw-r--r--features/steps/project/archived.rb36
-rw-r--r--features/steps/project/builds/summary.rb43
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/commits/revert.rb42
-rw-r--r--features/steps/project/ff_merge_requests.rb65
-rw-r--r--features/steps/project/fork.rb6
-rw-r--r--features/steps/project/forked_merge_requests.rb5
-rw-r--r--features/steps/project/issues/award_emoji.rb107
-rw-r--r--features/steps/project/issues/issues.rb2
-rw-r--r--features/steps/project/merge_requests.rb632
-rw-r--r--features/steps/project/merge_requests/acceptance.rb55
-rw-r--r--features/steps/project/merge_requests/revert.rb56
-rw-r--r--features/steps/project/project_group_links.rb51
-rw-r--r--features/steps/project/project_milestone.rb62
-rw-r--r--features/steps/project/project_shortcuts.rb42
-rw-r--r--features/steps/project/services.rb224
-rw-r--r--features/steps/project/snippets.rb100
-rw-r--r--features/steps/project/team_management.rb87
-rw-r--r--features/steps/project/wiki.rb195
-rw-r--r--features/steps/search.rb116
-rw-r--r--features/steps/shared/diff_note.rb2
-rw-r--r--features/steps/shared/issuable.rb12
-rw-r--r--features/support/env.rb2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/api_guard.rb24
-rw-r--r--lib/api/branches.rb23
-rw-r--r--lib/api/commits.rb18
-rw-r--r--lib/api/custom_attributes_endpoints.rb77
-rw-r--r--lib/api/entities.rb69
-rw-r--r--lib/api/helpers.rb40
-rw-r--r--lib/api/internal.rb9
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/merge_requests.rb11
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/projects.rb23
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/tags.rb12
-rw-r--r--lib/api/templates.rb8
-rw-r--r--lib/api/users.rb24
-rw-r--r--lib/api/v3/branches.rb8
-rw-r--r--lib/api/v3/commits.rb16
-rw-r--r--lib/api/v3/entities.rb4
-rw-r--r--lib/api/v3/merge_requests.rb4
-rw-r--r--lib/api/v3/repositories.rb4
-rw-r--r--lib/api/v3/tags.rb4
-rw-r--r--lib/api/v3/templates.rb8
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/banzai/filter/image_lazy_load_filter.rb3
-rw-r--r--lib/banzai/filter/markdown_filter.rb32
-rw-r--r--lib/banzai/filter/sanitization_filter.rb29
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb6
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb3
-rw-r--r--lib/banzai/renderer.rb7
-rw-r--r--lib/ci/ansi2html.rb331
-rw-r--r--lib/ci/charts.rb116
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb251
-rw-r--r--lib/ci/mask_secret.rb10
-rw-r--r--lib/ci/model.rb11
-rw-r--r--lib/declarative_policy/rule.rb20
-rw-r--r--lib/declarative_policy/runner.rb31
-rw-r--r--lib/github/client.rb3
-rw-r--r--lib/github/import.rb110
-rw-r--r--lib/github/import/issue.rb13
-rw-r--r--lib/github/import/legacy_diff_note.rb12
-rw-r--r--lib/github/import/merge_request.rb13
-rw-r--r--lib/github/import/note.rb13
-rw-r--r--lib/github/representation/branch.rb20
-rw-r--r--lib/github/representation/issuable.rb12
-rw-r--r--lib/github/representation/issue.rb20
-rw-r--r--lib/github/representation/pull_request.rb75
-rw-r--r--lib/gitlab/auth.rb22
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb65
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb53
-rw-r--r--lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb41
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb32
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb313
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb59
-rw-r--r--lib/gitlab/bare_repository_importer.rb3
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb9
-rw-r--r--lib/gitlab/ci/ansi2html.rb344
-rw-r--r--lib/gitlab/ci/build/policy.rb15
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb19
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb43
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb25
-rw-r--r--lib/gitlab/ci/charts.rb118
-rw-r--r--lib/gitlab/ci/mask_secret.rb12
-rw-r--r--lib/gitlab/ci/model.rb13
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb27
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb29
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb25
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb36
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb33
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb54
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb35
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/repository.rb30
-rw-r--r--lib/gitlab/ci/pipeline/duration.rb143
-rw-r--r--lib/gitlab/ci/pipeline_duration.rb141
-rw-r--r--lib/gitlab/ci/stage/seed.rb2
-rw-r--r--lib/gitlab/ci/trace.rb6
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb97
-rw-r--r--lib/gitlab/ci/trace/stream.rb21
-rw-r--r--lib/gitlab/ci/yaml_processor.rb189
-rw-r--r--lib/gitlab/closing_issue_extractor.rb5
-rw-r--r--lib/gitlab/conflict/parser.rb28
-rw-r--r--lib/gitlab/data_builder/push.rb9
-rw-r--r--lib/gitlab/database.rb9
-rw-r--r--lib/gitlab/database/migration_helpers.rb88
-rw-r--r--lib/gitlab/database/read_only_relation.rb16
-rw-r--r--lib/gitlab/diff/diff_refs.rb22
-rw-r--r--lib/gitlab/diff/file.rb33
-rw-r--r--lib/gitlab/diff/file_collection/base.rb5
-rw-r--r--lib/gitlab/diff/formatters/base_formatter.rb61
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb43
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb49
-rw-r--r--lib/gitlab/diff/image_point.rb23
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb3
-rw-r--r--lib/gitlab/diff/position.rb91
-rw-r--r--lib/gitlab/ee_compat_check.rb13
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb6
-rw-r--r--lib/gitlab/git.rb11
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/git/commit.rb6
-rw-r--r--lib/gitlab/git/commit_stats.rb19
-rw-r--r--lib/gitlab/git/committer.rb21
-rw-r--r--lib/gitlab/git/diff.rb50
-rw-r--r--lib/gitlab/git/hook.rb24
-rw-r--r--lib/gitlab/git/hooks_service.rb15
-rw-r--r--lib/gitlab/git/operation_service.rb29
-rw-r--r--lib/gitlab/git/popen.rb8
-rw-r--r--lib/gitlab/git/repository.rb395
-rw-r--r--lib/gitlab/git/rev_list.rb6
-rw-r--r--lib/gitlab/git/storage.rb1
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb30
-rw-r--r--lib/gitlab/git/storage/health.rb2
-rw-r--r--lib/gitlab/git/storage/null_circuit_breaker.rb47
-rw-r--r--lib/gitlab/git/user.rb27
-rw-r--r--lib/gitlab/git/wiki.rb115
-rw-r--r--lib/gitlab/git/wiki_file.rb19
-rw-r--r--lib/gitlab/git/wiki_page.rb39
-rw-r--r--lib/gitlab/git/wiki_page_version.rb19
-rw-r--r--lib/gitlab/git_access.rb12
-rw-r--r--lib/gitlab/git_access_wiki.rb5
-rw-r--r--lib/gitlab/git_ref_validator.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb176
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb87
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb39
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb79
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb22
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb5
-rw-r--r--lib/gitlab/gitaly_client/util.rb27
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/gpg.rb29
-rw-r--r--lib/gitlab/gpg/commit.rb10
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb4
-rw-r--r--lib/gitlab/group_hierarchy.rb15
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb6
-rw-r--r--lib/gitlab/i18n.rb3
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/kubernetes.rb2
-rw-r--r--lib/gitlab/ldap/access.rb2
-rw-r--r--lib/gitlab/ldap/adapter.rb22
-rw-r--r--lib/gitlab/ldap/auth_hash.rb4
-rw-r--r--lib/gitlab/ldap/dn.rb301
-rw-r--r--lib/gitlab/ldap/person.rb30
-rw-r--r--lib/gitlab/ldap/user.rb26
-rw-r--r--lib/gitlab/mail_room.rb27
-rw-r--r--lib/gitlab/markdown/pipeline.rb32
-rw-r--r--lib/gitlab/middleware/go.rb33
-rw-r--r--lib/gitlab/middleware/read_only.rb88
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/o_auth/user.rb72
-rw-r--r--lib/gitlab/pages.rb5
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/saml/user.rb37
-rw-r--r--lib/gitlab/shell.rb66
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb2
-rw-r--r--lib/gitlab/sql/union.rb9
-rw-r--r--lib/gitlab/url_sanitizer.rb33
-rw-r--r--lib/gitlab/usage_data.rb51
-rw-r--r--lib/gitlab/workhorse.rb13
-rw-r--r--lib/google_api/auth.rb54
-rw-r--r--lib/google_api/cloud_platform/client.rb88
-rw-r--r--lib/rspec_flaky/config.rb21
-rw-r--r--lib/rspec_flaky/flaky_example.rb21
-rw-r--r--lib/rspec_flaky/flaky_examples_collection.rb37
-rw-r--r--lib/rspec_flaky/listener.rb63
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb5
-rw-r--r--lib/system_check/app/git_version_check.rb2
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--lib/system_check/incoming_email/imap_authentication_check.rb45
-rw-r--r--lib/system_check/orphans/namespace_check.rb54
-rw-r--r--lib/system_check/orphans/repository_check.rb68
-rw-r--r--lib/tasks/gitlab/assets.rake2
-rw-r--r--lib/tasks/gitlab/check.rake29
-rw-r--r--lib/tasks/gitlab/dev.rake10
-rw-r--r--lib/tasks/gitlab/gitaly.rake7
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/storage.rake85
-rw-r--r--lib/tasks/import.rake27
-rw-r--r--locale/bg/gitlab.po342
-rw-r--r--locale/de/gitlab.po410
-rw-r--r--locale/en/gitlab.po2
-rw-r--r--locale/eo/gitlab.po342
-rw-r--r--locale/es/gitlab.po342
-rw-r--r--locale/fr/gitlab.po510
-rw-r--r--locale/gitlab.pot659
-rw-r--r--locale/it/gitlab.po342
-rw-r--r--locale/ja/gitlab.po342
-rw-r--r--locale/ko/gitlab.po342
-rw-r--r--locale/nl_NL/gitlab.po1750
-rw-r--r--locale/pt_BR/gitlab.po342
-rw-r--r--locale/ru/gitlab.po472
-rw-r--r--locale/uk/gitlab.po476
-rw-r--r--locale/zh_CN/gitlab.po390
-rw-r--r--locale/zh_HK/gitlab.po342
-rw-r--r--locale/zh_TW/gitlab.po376
-rw-r--r--package.json5
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock25
-rw-r--r--qa/README.md41
-rw-r--r--qa/qa.rb9
-rw-r--r--qa/qa/page/admin/menu.rb11
-rw-r--r--qa/qa/page/dashboard/groups.rb23
-rw-r--r--qa/qa/page/group/new.rb23
-rw-r--r--qa/qa/page/group/show.rb18
-rw-r--r--qa/qa/page/main/menu.rb21
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--qa/qa/runtime/namespace.rb4
-rw-r--r--qa/qa/runtime/user.rb2
-rw-r--r--qa/qa/scenario/gitlab/group/create.rb27
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb20
-rw-r--r--qa/qa/scenario/gitlab/sandbox/prepare.rb28
-rw-r--r--qa/qa/specs/config.rb3
-rw-r--r--rubocop/cop/rspec/verbose_include_metadata.rb74
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/lint-changelog-yaml22
-rwxr-xr-xscripts/lint-doc.sh18
-rw-r--r--scripts/schema_changed.sh10
-rwxr-xr-xscripts/static-analysis3
-rwxr-xr-xscripts/trigger-build-docs45
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb11
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb26
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb49
-rw-r--r--spec/controllers/health_controller_spec.rb1
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb35
-rw-r--r--spec/controllers/profiles_controller_spec.rb14
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb70
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb7
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb15
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb308
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb351
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb10
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb32
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb104
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb43
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb44
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb90
-rw-r--r--spec/controllers/projects_controller_spec.rb43
-rw-r--r--spec/controllers/registrations_controller_spec.rb64
-rw-r--r--spec/factories/ci/build_trace_section_names.rb6
-rw-r--r--spec/factories/ci/pipelines.rb1
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/emails.rb2
-rw-r--r--spec/factories/fork_networks.rb5
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/factories/gitaly/commit.rb17
-rw-r--r--spec/factories/gitaly/commit_author.rb9
-rw-r--r--spec/factories/gpg_key_subkeys.rb10
-rw-r--r--spec/factories/gpg_keys.rb4
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/projects.rb10
-rw-r--r--spec/factories/user_custom_attributes.rb7
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb10
-rw-r--r--spec/features/admin/admin_health_check_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb2
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb8
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb2
-rw-r--r--spec/features/admin/admin_users_spec.rb4
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb4
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/ci_lint_spec.rb2
-rw-r--r--spec/features/container_registry_spec.rb9
-rw-r--r--spec/features/copy_as_gfm_spec.rb132
-rw-r--r--spec/features/cycle_analytics_spec.rb2
-rw-r--r--spec/features/dashboard/active_tab_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb8
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb8
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb17
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb25
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb8
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb6
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/labels/subscription_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb22
-rw-r--r--spec/features/groups_spec.rb6
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb56
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb106
-rw-r--r--spec/features/issuables/user_sees_sidebar_spec.rb4
-rw-r--r--spec/features/issues/award_emoji_spec.rb18
-rw-r--r--spec/features/issues/award_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb4
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb18
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb4
-rw-r--r--spec/features/issues/form_spec.rb49
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb3
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb8
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb2
-rw-r--r--spec/features/issues/move_spec.rb6
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb2
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/issues_spec.rb202
-rw-r--r--spec/features/login_spec.rb8
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/award_spec.rb2
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb2
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_from_fork_spec.rb89
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb23
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb26
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb14
-rw-r--r--spec/features/merge_requests/discussion_lock_spec.rb49
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb4
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb8
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb20
-rw-r--r--spec/features/merge_requests/form_spec.rb12
-rw-r--r--spec/features/merge_requests/image_diff_notes.rb196
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb2
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb6
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/resolve_outdated_diff_discussions.rb2
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/toggle_whitespace_changes_spec.rb2
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb8
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb40
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb35
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_spec.rb61
-rw-r--r--spec/features/milestones/show_spec.rb4
-rw-r--r--spec/features/profile_spec.rb44
-rw-r--r--spec/features/profiles/emails_spec.rb71
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb14
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb2
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb2
-rw-r--r--spec/features/profiles/preferences_spec.rb63
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb2
-rw-r--r--spec/features/profiles/user_manages_emails_spec.rb78
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb68
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb15
-rw-r--r--spec/features/projects/artifacts/browse_spec.rb50
-rw-r--r--spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb104
-rw-r--r--spec/features/projects/badges/list_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb100
-rw-r--r--spec/features/projects/clusters_spec.rb111
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/commit/diff_notes_spec.rb36
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb56
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb4
-rw-r--r--spec/features/projects/diffs/diff_show_spec.rb53
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb206
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/fork_spec.rb57
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb56
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb37
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb32
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/features/projects/labels/subscription_spec.rb2
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb10
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb2
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb2
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb84
-rw-r--r--spec/features/projects/merge_requests/user_closes_merge_request_spec.rb21
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_commit_spec.rb19
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb172
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb50
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb25
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb22
-rw-r--r--spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb59
-rw-r--r--spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb63
-rw-r--r--spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_diffs_spec.rb46
-rw-r--r--spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb92
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb72
-rw-r--r--spec/features/projects/milestones/user_interacts_with_labels_spec.rb40
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb14
-rw-r--r--spec/features/projects/project_settings_spec.rb30
-rw-r--r--spec/features/projects/ref_switcher_spec.rb2
-rw-r--r--spec/features/projects/services/jira_service_spec.rb89
-rw-r--r--spec/features/projects/services/slack_service_spec.rb26
-rw-r--r--spec/features/projects/services/user_activates_asana_spec.rb24
-rw-r--r--spec/features/projects/services/user_activates_assembla_spec.rb23
-rw-r--r--spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb31
-rw-r--r--spec/features/projects/services/user_activates_emails_on_push_spec.rb23
-rw-r--r--spec/features/projects/services/user_activates_flowdock_spec.rb23
-rw-r--r--spec/features/projects/services/user_activates_hipchat_spec.rb38
-rw-r--r--spec/features/projects/services/user_activates_irker_spec.rb24
-rw-r--r--spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb26
-rw-r--r--spec/features/projects/services/user_activates_jira_spec.rb89
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb (renamed from spec/features/projects/services/mattermost_slash_command_spec.rb)0
-rw-r--r--spec/features/projects/services/user_activates_pivotaltracker_spec.rb23
-rw-r--r--spec/features/projects/services/user_activates_pushover_spec.rb27
-rw-r--r--spec/features/projects/services/user_activates_slack_notifications_spec.rb54
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb (renamed from spec/features/projects/services/slack_slash_command_spec.rb)0
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb25
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/user_manages_group_links_spec.rb41
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb68
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb2
-rw-r--r--spec/features/projects/shortcuts_spec.rb21
-rw-r--r--spec/features/projects/show_project_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_deletes_snippet_spec.rb20
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_views_snippets_spec.rb20
-rw-r--r--spec/features/projects/user_archives_project_spec.rb43
-rw-r--r--spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb20
-rw-r--r--spec/features/projects/user_browses_files_spec.rb8
-rw-r--r--spec/features/projects/user_creates_directory_spec.rb4
-rw-r--r--spec/features/projects/user_creates_files_spec.rb12
-rw-r--r--spec/features/projects/user_creates_project_spec.rb2
-rw-r--r--spec/features/projects/user_deletes_files_spec.rb6
-rw-r--r--spec/features/projects/user_edits_files_spec.rb60
-rw-r--r--spec/features/projects/user_interacts_with_stars_spec.rb2
-rw-r--r--spec/features/projects/user_replaces_files_spec.rb6
-rw-r--r--spec/features/projects/user_uploads_files_spec.rb13
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb108
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb227
-rw-r--r--spec/features/projects/wiki/user_deletes_wiki_page_spec.rb19
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb149
-rw-r--r--spec/features/projects/wiki/user_views_project_wiki_page_spec.rb39
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb140
-rw-r--r--spec/features/projects_spec.rb60
-rw-r--r--spec/features/protected_branches_spec.rb209
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb67
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb47
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb49
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb76
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb36
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb35
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb90
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb52
-rw-r--r--spec/features/search_spec.rb310
-rw-r--r--spec/features/security/project/internal_access_spec.rb15
-rw-r--r--spec/features/security/project/private_access_spec.rb15
-rw-r--r--spec/features/security/project/public_access_spec.rb15
-rw-r--r--spec/features/signup_spec.rb18
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb2
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb31
-rw-r--r--spec/features/task_lists_spec.rb8
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb16
-rw-r--r--spec/features/user_callout_spec.rb55
-rw-r--r--spec/features/users/snippets_spec.rb2
-rw-r--r--spec/features/users_spec.rb2
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb97
-rw-r--r--spec/finders/fork_projects_finder_spec.rb43
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb54
-rw-r--r--spec/finders/merge_requests_finder_spec.rb12
-rw-r--r--spec/finders/users_finder_spec.rb22
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json11
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit/detail.json11
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json2
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/admins.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basics.json4
-rw-r--r--spec/fixtures/api/schemas/registry/repositories.json6
-rw-r--r--spec/fixtures/api/schemas/registry/repository.json27
-rw-r--r--spec/fixtures/api/schemas/registry/tag.json28
-rw-r--r--spec/fixtures/api/schemas/registry/tags.json6
-rw-r--r--spec/fixtures/config/kubeconfig.yml2
-rw-r--r--spec/fixtures/trace/trace_with_sections15
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb26
-rw-r--r--spec/helpers/avatars_helper_spec.rb102
-rw-r--r--spec/helpers/commits_helper_spec.rb22
-rw-r--r--spec/helpers/diff_helper_spec.rb4
-rw-r--r--spec/helpers/groups_helper_spec.rb2
-rw-r--r--spec/helpers/icons_helper_spec.rb19
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb7
-rw-r--r--spec/helpers/page_layout_helper_spec.rb6
-rw-r--r--spec/helpers/projects_helper_spec.rb44
-rw-r--r--spec/helpers/submodule_helper_spec.rb6
-rw-r--r--spec/initializers/doorkeeper_spec.rb4
-rw-r--r--spec/initializers/secret_token_spec.rb18
-rw-r--r--spec/javascripts/abuse_reports_spec.js80
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js4
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js4
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js2
-rw-r--r--spec/javascripts/build_spec.js14
-rw-r--r--spec/javascripts/clusters_spec.js79
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js8
-rw-r--r--spec/javascripts/commits_spec.js108
-rw-r--r--spec/javascripts/cycle_analytics/banner_spec.js41
-rw-r--r--spec/javascripts/cycle_analytics/limit_warning_component_spec.js2
-rw-r--r--spec/javascripts/cycle_analytics/total_time_component_spec.js58
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js4
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js219
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_options_spec.js45
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js122
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js2
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js22
-rw-r--r--spec/javascripts/fixtures/clusters.rb34
-rw-r--r--spec/javascripts/fixtures/dashboard.rb35
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb6
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml2
-rw-r--r--spec/javascripts/fly_out_nav_spec.js15
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js (renamed from spec/javascripts/notes/stores/helpers.js)0
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js132
-rw-r--r--spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js139
-rw-r--r--spec/javascripts/image_diff/helpers/dom_helper_spec.js118
-rw-r--r--spec/javascripts/image_diff/helpers/utils_helper_spec.js207
-rw-r--r--spec/javascripts/image_diff/image_badge_spec.js84
-rw-r--r--spec/javascripts/image_diff/image_diff_spec.js361
-rw-r--r--spec/javascripts/image_diff/init_discussion_tab_spec.js37
-rw-r--r--spec/javascripts/image_diff/mock_data.js28
-rw-r--r--spec/javascripts/image_diff/replaced_image_diff_spec.js312
-rw-r--r--spec/javascripts/image_diff/view_types_spec.js24
-rw-r--r--spec/javascripts/issuable_context_spec.js34
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js25
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js693
-rw-r--r--spec/javascripts/lib/utils/csrf_token_spec.js49
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js32
-rw-r--r--spec/javascripts/lib/utils/sticky_spec.js75
-rw-r--r--spec/javascripts/line_highlighter_spec.js18
-rw-r--r--spec/javascripts/locale/sprintf_spec.js74
-rw-r--r--spec/javascripts/merge_request_spec.js39
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js30
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js24
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js25
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js49
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js9
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js35
-rw-r--r--spec/javascripts/monitoring/graph_spec.js24
-rw-r--r--spec/javascripts/monitoring/mock_data.js8
-rw-r--r--spec/javascripts/monitoring/monitoring_paths_spec.js34
-rw-r--r--spec/javascripts/monitoring/utils/multiple_time_series_spec.js15
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js54
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js3
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js30
-rw-r--r--spec/javascripts/notes_spec.js14
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js1
-rw-r--r--spec/javascripts/pipelines/error_state_spec.js6
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js19
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js2
-rw-r--r--spec/javascripts/pretty_time_spec.js282
-rw-r--r--spec/javascripts/profile/account/components/delete_account_modal_spec.js129
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js2
-rw-r--r--spec/javascripts/registry/components/app_spec.js122
-rw-r--r--spec/javascripts/registry/components/collapsible_container_spec.js58
-rw-r--r--spec/javascripts/registry/components/table_registry_spec.js49
-rw-r--r--spec/javascripts/registry/getters_spec.js43
-rw-r--r--spec/javascripts/registry/mock_data.js122
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js85
-rw-r--r--spec/javascripts/registry/stores/mutations_spec.js81
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js2
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js28
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js76
-rw-r--r--spec/javascripts/right_sidebar_spec.js116
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js36
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_spec.js41
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js71
-rw-r--r--spec/javascripts/todos_spec.js29
-rw-r--r--spec/javascripts/user_callout_spec.js49
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js20
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js36
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js113
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js15
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js201
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js42
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js34
-rw-r--r--spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js46
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb36
-rw-r--r--spec/lib/banzai/pipeline/email_pipeline_spec.rb14
-rw-r--r--spec/lib/banzai/renderer_spec.rb9
-rw-r--r--spec/lib/ci/ansi2html_spec.rb220
-rw-r--r--spec/lib/ci/charts_spec.rb24
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb1697
-rw-r--r--spec/lib/ci/mask_secret_spec.rb27
-rw-r--r--spec/lib/github/client_spec.rb34
-rw-r--r--spec/lib/github/import/legacy_diff_note_spec.rb9
-rw-r--r--spec/lib/github/import/note_spec.rb9
-rw-r--r--spec/lib/gitlab/auth_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb117
-rw-r--r--spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb40
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb122
-rw-r--r--spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb36
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb93
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb52
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb246
-rw-r--r--spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/build/policy_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/charts_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/mask_secret_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb142
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb130
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb115
-rw-r--r--spec/lib/gitlab/ci/pipeline_duration_spec.rb115
-rw-r--r--spec/lib/gitlab/ci/stage/seed_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/trace/section_parser_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb1716
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb44
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb122
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb4
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/diff_refs_spec.rb55
-rw-r--r--spec/lib/gitlab/diff/formatters/image_formatter_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/formatters/text_formatter_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb117
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb26
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb61
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb7
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb54
-rw-r--r--spec/lib/gitlab/git/committer_spec.rb22
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb3
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb38
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb11
-rw-r--r--spec/lib/gitlab/git/hooks_service_spec.rb8
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb179
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb4
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb13
-rw-r--r--spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb77
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb2
-rw-r--r--spec/lib/gitlab/git/user_spec.rb38
-rw-r--r--spec/lib/gitlab/git_access_spec.rb27
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb29
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb5
-rw-r--r--spec/lib/gitlab/git_spec.rb9
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb29
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb92
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb43
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb138
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb39
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb49
-rw-r--r--spec/lib/gitlab/gpg_spec.rb17
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb15
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml9
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml31
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb24
-rw-r--r--spec/lib/gitlab/ldap/dn_spec.rb224
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb33
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb16
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb18
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb142
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb7
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb17
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb7
-rw-r--r--spec/lib/gitlab/popen_spec.rb2
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb97
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb139
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb7
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb34
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb41
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb65
-rw-r--r--spec/lib/google_api/auth_spec.rb41
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb128
-rw-r--r--spec/lib/rspec_flaky/config_spec.rb102
-rw-r--r--spec/lib/rspec_flaky/flaky_example_spec.rb129
-rw-r--r--spec/lib/rspec_flaky/flaky_examples_collection_spec.rb79
-rw-r--r--spec/lib/rspec_flaky/listener_spec.rb173
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb15
-rw-r--r--spec/lib/system_check/base_check_spec.rb17
-rw-r--r--spec/lib/system_check/orphans/namespace_check_spec.rb61
-rw-r--r--spec/lib/system_check/orphans/repository_check_spec.rb68
-rw-r--r--spec/mailers/emails/profile_spec.rb25
-rw-r--r--spec/mailers/notify_spec.rb491
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb5
-rw-r--r--spec/migrations/clean_stages_statuses_migration_spec.rb51
-rw-r--r--spec/migrations/delete_conflicting_redirect_routes_spec.rb58
-rw-r--r--spec/migrations/normalize_ldap_extern_uids_spec.rb56
-rw-r--r--spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb43
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb59
-rw-r--r--spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb22
-rw-r--r--spec/migrations/update_notes_type_for_import_spec.rb22
-rw-r--r--spec/migrations/update_upload_paths_to_system_spec.rb4
-rw-r--r--spec/models/abuse_report_spec.rb7
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb22
-rw-r--r--spec/models/chat_name_spec.rb3
-rw-r--r--spec/models/chat_team_spec.rb3
-rw-r--r--spec/models/ci/artifact_blob_spec.rb50
-rw-r--r--spec/models/ci/build_spec.rb60
-rw-r--r--spec/models/ci/build_trace_section_name_spec.rb12
-rw-r--r--spec/models/ci/build_trace_section_spec.rb11
-rw-r--r--spec/models/ci/pipeline_spec.rb31
-rw-r--r--spec/models/ci/pipeline_variable_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb69
-rw-r--r--spec/models/commit_spec.rb7
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb72
-rw-r--r--spec/models/concerns/has_status_spec.rb12
-rw-r--r--spec/models/concerns/routable_spec.rb10
-rw-r--r--spec/models/diff_note_spec.rb40
-rw-r--r--spec/models/email_spec.rb29
-rw-r--r--spec/models/environment_spec.rb22
-rw-r--r--spec/models/fork_network_member_spec.rb8
-rw-r--r--spec/models/fork_network_spec.rb54
-rw-r--r--spec/models/forked_project_link_spec.rb12
-rw-r--r--spec/models/gcp/cluster_spec.rb240
-rw-r--r--spec/models/gpg_key_spec.rb56
-rw-r--r--spec/models/gpg_key_subkey_spec.rb15
-rw-r--r--spec/models/gpg_signature_spec.rb52
-rw-r--r--spec/models/key_spec.rb45
-rw-r--r--spec/models/lfs_objects_project_spec.rb15
-rw-r--r--spec/models/member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb97
-rw-r--r--spec/models/milestone_spec.rb15
-rw-r--r--spec/models/namespace_spec.rb51
-rw-r--r--spec/models/note_spec.rb50
-rw-r--r--spec/models/personal_access_token_spec.rb35
-rw-r--r--spec/models/project_auto_devops_spec.rb16
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb22
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb15
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb12
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb2
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb37
-rw-r--r--spec/models/project_spec.rb266
-rw-r--r--spec/models/project_wiki_spec.rb64
-rw-r--r--spec/models/push_event_spec.rb88
-rw-r--r--spec/models/repository_spec.rb439
-rw-r--r--spec/models/sent_notification_spec.rb122
-rw-r--r--spec/models/user_custom_attribute_spec.rb16
-rw-r--r--spec/models/user_spec.rb289
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/policies/gcp/cluster_policy_spec.rb28
-rw-r--r--spec/policies/global_policy_spec.rb37
-rw-r--r--spec/policies/group_policy_spec.rb18
-rw-r--r--spec/policies/issuable_policy_spec.rb28
-rw-r--r--spec/policies/namespace_policy_spec.rb20
-rw-r--r--spec/policies/note_policy_spec.rb71
-rw-r--r--spec/policies/project_policy_spec.rb137
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb17
-rw-r--r--spec/presenters/gcp/cluster_presenter_spec.rb35
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb4
-rw-r--r--spec/requests/api/access_requests_spec.rb12
-rw-r--r--spec/requests/api/award_emoji_spec.rb14
-rw-r--r--spec/requests/api/boards_spec.rb33
-rw-r--r--spec/requests/api/branches_spec.rb12
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb11
-rw-r--r--spec/requests/api/commits_spec.rb5
-rw-r--r--spec/requests/api/environments_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/requests/api/helpers_spec.rb127
-rw-r--r--spec/requests/api/issues_spec.rb11
-rw-r--r--spec/requests/api/merge_requests_spec.rb77
-rw-r--r--spec/requests/api/notes_spec.rb34
-rw-r--r--spec/requests/api/projects_spec.rb63
-rw-r--r--spec/requests/api/runner_spec.rb3
-rw-r--r--spec/requests/api/services_spec.rb13
-rw-r--r--spec/requests/api/users_spec.rb64
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb14
-rw-r--r--spec/requests/api/v3/boards_spec.rb25
-rw-r--r--spec/requests/api/v3/branches_spec.rb10
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb6
-rw-r--r--spec/requests/api/v3/builds_spec.rb14
-rw-r--r--spec/requests/api/v3/issues_spec.rb19
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb40
-rw-r--r--spec/requests/api/v3/projects_spec.rb1
-rw-r--r--spec/requests/jwt_controller_spec.rb4
-rw-r--r--spec/requests/lfs_http_spec.rb34
-rw-r--r--spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb53
-rw-r--r--spec/serializers/build_details_entity_spec.rb10
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/cluster_entity_spec.rb22
-rw-r--r--spec/serializers/cluster_serializer_spec.rb19
-rw-r--r--spec/serializers/container_repository_entity_spec.rb41
-rw-r--r--spec/serializers/container_tag_entity_spec.rb43
-rw-r--r--spec/serializers/environment_entity_spec.rb4
-rw-r--r--spec/serializers/merge_request_entity_spec.rb13
-rw-r--r--spec/serializers/pipeline_entity_spec.rb13
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb12
-rw-r--r--spec/serializers/status_entity_spec.rb4
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb128
-rw-r--r--spec/services/ci/extract_sections_from_build_trace_service_spec.rb55
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/fetch_kubernetes_token_service_spec.rb64
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb2
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/retry_build_service_spec.rb11
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb6
-rw-r--r--spec/services/deploy_keys/create_service_spec.rb12
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb6
-rw-r--r--spec/services/emails/confirm_service_spec.rb15
-rw-r--r--spec/services/emails/create_service_spec.rb7
-rw-r--r--spec/services/emails/destroy_service_spec.rb4
-rw-r--r--spec/services/event_create_service_spec.rb8
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb31
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb43
-rw-r--r--spec/services/keys/create_service_spec.rb21
-rw-r--r--spec/services/keys/last_used_service_spec.rb63
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb20
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb84
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb23
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb11
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb12
-rw-r--r--spec/services/merge_requests/update_service_spec.rb11
-rw-r--r--spec/services/notification_service_spec.rb13
-rw-r--r--spec/services/projects/count_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb86
-rw-r--r--spec/services/projects/destroy_service_spec.rb13
-rw-r--r--spec/services/projects/fork_service_spec.rb44
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb74
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb7
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb30
-rw-r--r--spec/services/projects/update_service_spec.rb76
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/services/tags/create_service_spec.rb2
-rw-r--r--spec/services/users/activity_service_spec.rb12
-rw-r--r--spec/services/users/last_push_event_service_spec.rb112
-rw-r--r--spec/services/users/update_service_spec.rb8
-rw-r--r--spec/spec_helper.rb37
-rw-r--r--spec/support/api/scopes/read_user_shared_examples.rb4
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb2
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb6
-rw-r--r--spec/support/gpg_helpers.rb313
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb28
-rw-r--r--spec/support/ldap_shared_examples.rb69
-rw-r--r--spec/support/matchers/navigation_matcher.rb12
-rw-r--r--spec/support/migrations_helpers.rb4
-rw-r--r--spec/support/project_forks_helper.rb58
-rw-r--r--spec/support/query_recorder.rb36
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb (renamed from spec/support/project_features_apply_to_issuables_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/features/search_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/position_formatters.rb43
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb103
-rw-r--r--spec/support/stub_configuration.rb2
-rw-r--r--spec/support/stub_gitlab_calls.rb4
-rw-r--r--spec/support/test_env.rb93
-rw-r--r--spec/support/update_invalid_issuable.rb27
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb17
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb52
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb20
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb4
-rw-r--r--spec/views/dashboard/projects/_nav.html.haml.rb17
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb24
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb5
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb9
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb11
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb62
-rw-r--r--spec/views/projects/registry/repositories/index.html.haml_spec.rb36
-rw-r--r--spec/views/shared/issuable/_participants.html.haml.rb26
-rw-r--r--spec/views/shared/milestones/_issuable.html.haml.rb19
-rw-r--r--spec/workers/build_finished_worker_spec.rb2
-rw-r--r--spec/workers/build_trace_sections_worker_spec.rb23
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb23
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb15
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb8
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb10
-rw-r--r--spec/workers/post_receive_spec.rb13
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb29
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb7
-rw-r--r--spec/workers/storage_migrator_worker_spec.rb30
-rw-r--r--spec/workers/use_key_worker_spec.rb23
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb67
-rw-r--r--symbol/icons.svg1
-rw-r--r--symbol/sprite.symbol.html177
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/CONTRIBUTING.md50
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml3
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml3
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml3
-rw-r--r--vendor/licenses.csv619
-rw-r--r--yarn.lock172
2440 files changed, 54049 insertions, 22505 deletions
diff --git a/.eslintignore b/.eslintignore
index 1605e483e9e..1623b996213 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,4 +7,5 @@
/vendor/
karma.config.js
webpack.config.js
-/app/assets/javascripts/locale/**/*.js
+svg.config.js
+/app/assets/javascripts/locale/**/app.js
diff --git a/.flayignore b/.flayignore
index b63ce4c4df0..acac0ce14c9 100644
--- a/.flayignore
+++ b/.flayignore
@@ -5,3 +5,4 @@ app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
+lib/gitlab/gitaly_client/operation_service.rb
diff --git a/.gitignore b/.gitignore
index 3baf640a9c3..4933575332b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,4 +63,5 @@ eslint-report.html
/.gitlab_workhorse_secret
/webpack-report/
/locale/**/LC_MESSAGES
+/locale/**/*.time_stamp
/.rspec
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index dadce073309..7eb9e5ab6de 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.13-phantomjs-2.1-node-7.1-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
@@ -27,7 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
- FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
+ FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script:
- bundle --version
@@ -87,12 +87,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
+ - export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- - cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
+ - '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
@@ -128,7 +129,7 @@ stages:
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
@@ -174,12 +175,22 @@ build-package:
# Review docs base
.review-docs: &review-docs
image: ruby:2.4-alpine
- before_script: []
+ before_script:
+ - gem install gitlab --no-doc
+ # We need to download the script rather than clone the repo since the
+ # review-docs-cleanup job will not be able to run when the branch gets
+ # deleted (when merging the MR).
+ - apk add --update openssl
+ - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
+ - chmod 755 trigger-build-docs
services: []
+ cache: {}
+ dependencies: []
+ artifacts: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
- cache: {}
+ GIT_STRATEGY: none
when: manual
only:
- branches
@@ -191,10 +202,12 @@ review-docs-deploy:
stage: build
environment:
name: review-docs/$CI_COMMIT_REF_NAME
+ # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
+ # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
+ url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- - gem install gitlab --no-doc
- - scripts/trigger-build-docs deploy
+ - ./trigger-build-docs deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
@@ -204,8 +217,7 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- - gem install gitlab --no-doc
- - scripts/trigger-build-docs cleanup
+ - ./trigger-build-docs cleanup
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
@@ -222,7 +234,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- - mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
+ - mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@@ -241,24 +253,24 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
+ - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
+ - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
services: []
before_script: []
- cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
- NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
+ NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
+ retry: 0
only:
- branches
except:
@@ -270,7 +282,7 @@ flaky-examples-check:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- - scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
+ - scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
@@ -393,6 +405,7 @@ docs lint:
before_script: []
script:
- scripts/lint-doc.sh
+ - scripts/lint-changelog-yaml
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
@@ -410,13 +423,14 @@ downtime_check:
ee_compat_check:
<<: *rake-exec
- only:
- - branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?/
+ - branches@gitlab-org/gitlab-ee
+ - branches@gitlab/gitlab-ee
allow_failure: yes
+ retry: 0
cache:
key: "ee_compat_check_repo"
paths:
@@ -448,6 +462,7 @@ db:migrate:reset-mysql:
.migration-paths: &migration-paths
<<: *dedicated-runner
<<: *pull-cache
+ <<: *except-docs
stage: test
variables:
SETUP_DB: "false"
@@ -513,6 +528,12 @@ db:seed_fu-mysql:
<<: *db-seed_fu
<<: *use-mysql
+db:check-schema-pg:
+ <<: *db-migrate-reset
+ <<: *use-pg
+ script:
+ - source scripts/schema_changed.sh
+
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
@@ -542,7 +563,7 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-60.0-node-7.1-postgresql-9.6"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
diff --git a/.rubocop.yml b/.rubocop.yml
index 4640681379a..dbeb1880d39 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 14
+ Max: 13
# Limit lines to 80 characters.
Metrics/LineLength:
@@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 17
+ Max: 15
# Lint ########################################################################
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cedfa60b3e..efd32d44890 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,250 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.0.3 (2017-10-05)
+
+- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
+- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
+- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
+- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
+- [FIXED] Improves UX of autodevops popover to match gpg one.
+- [FIXED] Fixed commenting on side-by-side commit diff.
+- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
+- [FIXED] Fix merge request counter updates after merge.
+- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
+- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
+- [FIXED] Ensure all refs are restored on a restore from backup.
+- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
+- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
+- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
+
+## 10.0.2 (2017-09-27)
+
+- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
+- [FIXED] Some checks in `rake gitlab:check` were failling with 'undefined method `run_command`'. !14469
+- [FIXED] Make locked setting of Runner to not affect jobs scheduling. !14483
+- [FIXED] Re-allow `name` attribute on user-provided anchor HTML.
+
+## 10.0.1 (2017-09-23)
+
+- [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration.
+
+## 10.0.0 (2017-09-22)
+
+- [SECURITY] Upgrade brace-expansion NPM package due to security issue. !13665 (Markus Koller)
+- [REMOVED] Remove CI API v1.
+- [FIXED] Ensure correct visibility level options shown on all Project, Group, and Snippets forms. !13442
+- [FIXED] Fix the /projects/:id/repository/files/:file_path/raw endpoint to handle dots in the file_path. !13512 (mahcsig)
+- [FIXED] Merge request reference in merge commit changed to full reference. !13518 (haseebeqx)
+- [FIXED] Removes Sortable default scope. !13558
+- [FIXED] Wiki table of contents are now properly nested to reflect header level. !13650 (Akihiro Nakashima)
+- [FIXED] Improve bare project import: Allow subgroups, take default visibility level into account. !13670
+- [FIXED] Fix group and project search for anonymous users. !13745
+- [FIXED] Fix searching for files by path. !13798
+- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler)
+- [FIXED] Fix incorrect date/time formatting on prometheus graphs. !13865
+- [FIXED] Changes the password change workflow for admins. !13901
+- [FIXED] API: Respect default group visibility when creating a group. !13903 (Robert Schilling)
+- [FIXED] Unescape HTML characters in Wiki title. !13942 (Jacopo Beschi @jacopo-beschi)
+- [FIXED] Make blob viewer for rich contents wider for mobile. !14011 (Takuya Noguchi)
+- [FIXED] Fix typo in the API Deploy Keys documentation page. !14014 (Vitaliy @blackst0ne Klachkov)
+- [FIXED] Hide admin link from default search results for non-admins. !14015
+- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083
+- [FIXED] Fix stray OR in New Project page. !14096 (Robin Bobbitt)
+- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108
+- [FIXED] Fix the diff file header from being html escaped for renamed files. !14121
+- [FIXED] Image attachments are properly displayed in notification emails again. !14161
+- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel)
+- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
+- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244
+- [FIXED] Allow using newlines in pipeline email service recipients. !14250
+- [FIXED] Fix errors when moving issue with reference to a group milestone. !14294
+- [FIXED] Fix the "resolve discussion in a new issue" button. !14357
+- [FIXED] File uploaders do not perform hard check, only soft check.
+- [FIXED] Add to_project_id parameter to Move Issue via API example.
+- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved.
+- [FIXED] Fixed add diff note button not showing after deleting a comment.
+- [FIXED] Fix broken svg in jobs dropdown for success status.
+- [FIXED] Fix buttons with different height in merge request widget.
+- [FIXED] Removes disabled state from dashboard project button.
+- [FIXED] Better align fallback image emojis.
+- [FIXED] Remove focus styles from dropdown empty links.
+- [FIXED] Fix inconsistent spacing for edit buttons on issues and merge request page.
+- [FIXED] Fix edit merge request and issues button inconsistent letter casing.
+- [FIXED] Improve Import/Export memory usage.
+- [FIXED] Fix Import/Export issue to do with fork merge requests.
+- [FIXED] Fix invite by email address duplication.
+- [FIXED] Adds tooltip to the branch name and improves performance.
+- [FIXED] Disable GitLab Project Import Button if source disabled.
+- [FIXED] Migrate issues authored by deleted user to the Ghost user.
+- [FIXED] Fix new navigation wrapping and causing height to grow.
+- [FIXED] Normalize styles for empty state combo button.
+- [FIXED] Fix external link to Composer website.
+- [FIXED] Prevents jobs dropdown from closing in pipeline graph.
+- [FIXED] Include the `is_admin` field in the `GET /users/:id` API when current user is an admin.
+- [FIXED] Fix breadcrumbs container in issue boards.
+- [FIXED] Fix project feature being deleted when updating project with invalid visibility level.
+- [FIXED] Truncate milestone title if sidebar is collapsed.
+- [FIXED] Prevents rendering empty badges when request fails.
+- [FIXED] Fixes margins on the top buttons of the pipeline table.
+- [FIXED] Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies.
+- [FIXED] Eliminate N+1 queries in loading discussions.json endpoint.
+- [FIXED] Eliminate N+1 queries referencing issues.
+- [FIXED] Remove unnecessary loading of discussions in `IssuesController#show`.
+- [FIXED] Fix errors thrown in merge request widget with external CI service/integration.
+- [FIXED] Do not show the Auto DevOps banner when the project has a .gitlab-ci.yml on master.
+- [FIXED] Reword job to pipeline to reflect what the graphs are really about.
+- [FIXED] Sort templates in the dropdown.
+- [FIXED] Fix Auto DevOps banner to be shown on empty projects.
+- [FIXED] Resolve Image onion skin + swipe does not work anymore.
+- [FIXED] Fix mini graph pipeline breakin in merge request view.
+- [FIXED] Fixed merge request changes bar jumping.
+- [FIXED] Improve migrations using triggers.
+- [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression.
+- [FIXED] disabling notifications globally now properly turns off group/project added
+ emails !13325
+- [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930
+- [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi)
+- [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov)
+- [CHANGED] Add filtered search to group merge requests dashboard. !13688 (Hiroyuki Sato)
+- [CHANGED] Fire hooks asynchronously when creating a new job to improve performance. !13734
+- [CHANGED] Improve performance for AutocompleteController#users.json. !13754 (Hiroyuki Sato)
+- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel)
+- [CHANGED] Support a multi-word fuzzy seach issues/merge requests on search bar. !13780 (Hiroyuki Sato)
+- [CHANGED] Default LDAP config "verify_certificates" to true for security. !13915
+- [CHANGED] "Share with group lock" now applies to subgroups, but owner can override setting on subgroups. !13944
+- [CHANGED] Make Gitaly PostUploadPack mandatory. !13953
+- [CHANGED] Remove project select dropdown from breadcrumb. !14010
+- [CHANGED] Redesign project feature permissions settings. !14062
+- [CHANGED] Document version Group Milestones API introduced.
+- [CHANGED] Finish migration to the new events setup.
+- [CHANGED] restyling of OAuth authorization confirmation. (Jacopo Beschi @jacopo-beschi)
+- [CHANGED] Added support for specific labels and colors.
+- [CHANGED] Move "Move issue" controls to right-sidebar.
+- [CHANGED] Remove pages settings when not available.
+- [CHANGED] Allow all AutoDevOps banners to be turned off.
+- [CHANGED] Update Rails project template to use Postgresql by default.
+- [CHANGED] Added support the multiple time series for prometheus monitoring.
+- [ADDED] API: Respect the "If-Unmodified-Since" header when delting a resource. !9621 (Robert Schilling)
+- [ADDED] Protected runners. !13194
+- [ADDED] Add support for copying permalink to notes via more actions dropdown. !13299
+- [ADDED] Add API support for wiki pages. !13372 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Add a `Last 7 days` option for Cycle Analytics view. !13443 (Mehdi Lahmam (@mehlah))
+- [ADDED] inherits milestone and labels when a merge request is created from issue. !13461 (haseebeqx)
+- [ADDED] Add 'from commit' information to cherry-picked commits. !13475 (Saverio Miroddi)
+- [ADDED] Add an option to list only archived projects. !13492 (Mehdi Lahmam (@mehlah))
+- [ADDED] Extend API: Pipeline Schedule Variable. !13653
+- [ADDED] Add settings for minimum SSH key strength and allowed key type. !13712 (Cory Hinshaw)
+- [ADDED] Add div id to the readme in the project overview. !13735 (Riccardo Padovani @rpadovani)
+- [ADDED] Add CI/CD job predefined variables with user name and login. !13824
+- [ADDED] API: Add GPG key management. !13828 (Robert Schilling)
+- [ADDED] Add CI/CD active kubernetes job policy. !13849
+- [ADDED] Add dropdown to Projects nav item. !13866
+- [ADDED] Allow users and administrator to configure Auto-DevOps. !13923
+- [ADDED] Implement `failure_reason` on `ci_builds`. !13937
+- [ADDED] Add branch existence check to the APIv4 branches via HEAD request. !13979 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Add quick submission on user settings page. !14007 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Add my_reaction_emoji param to /issues and /merge_requests API. !14016 (Hiroyuki Sato)
+- [ADDED] Make it possible to download a single job artifact file using the API. !14027
+- [ADDED] Add repository toggle for automatically resolving outdated diff discussions. !14053 (AshleyDumaine)
+- [ADDED] Scripts to detect orphaned repositories. !14204
+- [ADDED] Created callout for auto devops.
+- [ADDED] Add option in preferences to change navigation theme color.
+- [ADDED] Add JSON logger in `log/api_json.log` for Grape API endpoints.
+- [ADDED] Add CI_PIPELINE_SOURCE variable on CI Jobs.
+- [ADDED] Changed message and title on the 404 page. (Branka Martinovic)
+- [ADDED] Handle if Auto DevOps domain is not set in project settings.
+- [ADDED] Add collapsable sections for Pipeline Settings.
+- [OTHER] Add badge for dependency status. !13588 (Markus Koller)
+- [OTHER] Migration to remove pending delete projects with non-existing namespace. !13598
+- [OTHER] Bump rouge to v2.2.0. !13633
+- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685
+- [OTHER] Replace 'source/search_code.feature' spinach test with an rspec analog. !13697 (blackst0ne)
+- [OTHER] Remove unwanted refs after importing a project. !13766
+- [OTHER] Never wait for sidekiq jobs when creating projects. !13775
+- [OTHER] Gitaly feature toggles are on by default in development. !13802
+- [OTHER] Remove `is_` prefix from predicate method names. !13810 (Maxim Rydkin)
+- [OTHER] Update 'Using Docker images' documentation. !13848
+- [OTHER] Update gpg documentation with gpg2. !13851 (M M Arif)
+- [OTHER] Replace 'project/star.feature' spinach test with an rspec analog. !13855 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace 'project/user_lookup.feature' spinach test with an rspec analog. !13863 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Bump rouge to v2.2.1. !13887
+- [OTHER] Add documentation for PlantUML in reStructuredText. !13900 (Markus Koller)
+- [OTHER] Decrease ABC threshold to 55.25. !13904 (Maxim Rydkin)
+- [OTHER] Decrease Cyclomatic Complexity threshold to 14. !13972 (Maxim Rydkin)
+- [OTHER] Update documentation for confidential issue. !14117
+- [OTHER] Remove redundant WHERE from event queries.
+- [OTHER] Memoize the latest builds of a pipeline on a project's homepage.
+- [OTHER] Re-use issue/MR counts for the pagination system.
+- [OTHER] Memoize pipelines for project download buttons.
+- [OTHER] Reorganize indexes for the "deployments" table.
+- [OTHER] Improves markdown rendering performance for commit lists.
+- [OTHER] Only update the sidebar count caches when needed.
+- [OTHER] Improves performance of vue code by using vue files and moving svg out of data function in pipeline schedule callout.
+- [OTHER] Rework how recent push events are retrieved.
+- [OTHER] Restyle dropdown menus to make them look consistent.
+- [OTHER] Upgrade grape to 1.0.
+- [OTHER] Add usage data for Auto DevOps.
+- [OTHER] Cache the number of open issues and merge requests.
+- [OTHER] Constrain environment deployments to project IDs.
+- [OTHER] Eager load namespace owners for project dashboards.
+- [OTHER] Add description template examples to documentation.
+- [OTHER] Disallow NULL values for environments.project_id.
+- Add my reaction filter to search bar. !12962 (Hiroyuki Sato)
+- Generalize profile updates from providers. !12968 (Alexandros Keramidas)
+- Validate PO-files in static analysis. !13000
+- First-time contributor badge. !13143 (Micaël Bergeron <micaelbergeron@gmail.com>)
+- Add option to disable project export on instance. !13211 (Robin Bobbitt)
+- Hashed Storage support for Repositories (EXPERIMENTAL). !13246
+- Added tests for commits API unauthenticated user and public/private project. !13287 (Jacopo Beschi @jacopo-beschi)
+- Fix CI_PROJECT_PATH_SLUG slugify. !13350 (Ivan Chernov)
+- Add checks for branch existence before changing HEAD. !13359 (Vitaliy @blackst0ne Klachkov)
+- Fix the alignment of line numbers to lines of code in code viewer. !13403 (Trevor Flynn)
+- Allow users to move issues to other projects using a / command. !13436 (Manolis Mavrofidis)
+- Bumps omniauth-ldap gem version to 2.0.4. !13465
+- Implement the Gitaly RefService::RefExists endpoint. !13528 (Andrew Newdigate)
+- Changed all font-weight values to 400 and 600 and introduced 2 variables to manage them.
+- Simplify checking if objects exist code in new issaubles workers.
+- Present enqueued merge jobs as Merging as well.
+- Don't escape html entities in InlineDiffMarkdownMarker.
+- Move ConvDev Index location to after Cohorts.
+- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
+- [BUGIFX] Improves subgroup creation permissions. !13418
+
+## 9.5.8 (2017-10-04)
+
+- [FIXED] Fixed fork button being disabled for users who can fork to a group.
+
+## 9.5.7 (2017-10-03)
+
+- Fix gitlab rake:import:repos task.
+
+## 9.5.6 (2017-09-29)
+
+- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
+- [FIXED] Fix errors thrown in merge request widget with external CI service/integration.
+- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved.
+- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
+
+## 9.5.5 (2017-09-18)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler)
+- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083
+- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108
+- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel)
+- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244
+- [FIXED] Fix project feature being deleted when updating project with invalid visibility level.
+- [FIXED] Fix new navigation wrapping and causing height to grow.
+- [FIXED] Fix buttons with different height in merge request widget.
+- [FIXED] Normalize styles for empty state combo button.
+- [FIXED] Fix broken svg in jobs dropdown for success status.
+- [FIXED] Improve migrations using triggers.
+- [FIXED] Disable GitLab Project Import Button if source disabled.
+- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel)
+- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685
+- [OTHER] Update documentation for confidential issue. !14117
+
## 9.5.4 (2017-09-06)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6fb2c6bd1dc..6b261bd5938 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
-Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute).
+Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
@@ -101,7 +101,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start,
-look for [issues with the label `Accepting Merge Requests` and weight < 5][accepting-mrs-weight].
+look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
@@ -209,8 +209,7 @@ We add the ~"Accepting Merge Requests" label to:
- Low priority ~bug issues (i.e. we do not add it to the bugs that we want to
solve in the ~"Next Patch Release")
-- Small ~"feature proposal" that do not need ~UX / ~"Product work", or for which
-the ~UX / ~"Product work" is already done
+- Small ~"feature proposal"
- Small ~"technical debt" issues
After adding the ~"Accepting Merge Requests" label, we try to estimate the
@@ -223,6 +222,17 @@ know how difficult the issue is. Additionally:
- We encourage people that have never contributed to any open source project to
look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers]
+If you've decided that you would like to work on an issue, please @-mention
+the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what)
+as soon as possible. The product manager will then pull in appropriate GitLab team
+members to further discuss scope, design, and technical considerations. This will
+ensure that that your contribution is aligned with the GitLab product and minimize
+any rework and delay in getting it merged into master.
+
+GitLab team members who apply the ~"Accepting Merge Requests" label to an issue
+should update the issue description with a responsible product manager, inviting
+any potential community contributor to @-mention per above.
+
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened
[firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1
@@ -286,7 +296,10 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
-For changes in the interface, it can be helpful to create a mockup first.
+For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
+be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
+need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
+
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
@@ -421,7 +434,7 @@ request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`
-1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
+1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ca75280b09b..8298bb08b2d 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.38.0
+0.43.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 8f0916f768f..a918a2aa18d 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.5.0
+0.6.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index b3d91f9cfc0..5508e17c23b 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1,2 @@
-5.9.0
+5.9.3
+
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4a36342fcab..944880fa15e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.0.0
+3.2.0
diff --git a/Gemfile b/Gemfile
index a022319ae2c..5645d1ea4ff 100644
--- a/Gemfile
+++ b/Gemfile
@@ -23,10 +23,10 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
-gem 'doorkeeper-openid_connect', '~> 1.1.0'
+gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
-gem 'omniauth-azure-oauth2', '~> 0.0.6'
+gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
@@ -105,7 +105,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
# for Google storage
-gem 'google-api-client', '~> 0.8.6'
+gem 'google-api-client', '~> 0.13.6'
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -116,7 +116,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '2.0.0'
-gem 'gitlab-markup', '~> 1.5.1'
+gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
@@ -128,7 +128,7 @@ gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
-gem 'nokogiri', '~> 1.8.0'
+gem 'nokogiri', '~> 1.8.1'
# Diffs
gem 'diffy', '~> 3.1.0'
@@ -239,7 +239,7 @@ gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6'
gem 'uglifier', '~> 2.7.2'
-gem 'addressable', '~> 2.3.8'
+gem 'addressable', '~> 2.5.2'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
@@ -356,12 +356,13 @@ end
group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0'
- gem 'json-schema', '~> 2.6.2'
+ gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
+ gem 'test-prof', '~> 0.2.5'
end
gem 'octokit', '~> 4.6.2'
@@ -397,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
@@ -407,4 +408,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
-gem 'grape_logging', '~> 1.6'
+gem 'grape_logging', '~> 1.7'
diff --git a/Gemfile.lock b/Gemfile.lock
index d7e1c7581d5..6040596dda5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -45,7 +45,8 @@ GEM
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
- addressable (2.3.8)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
@@ -62,10 +63,6 @@ GEM
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
- autoparse (0.3.3)
- addressable (>= 2.3.1)
- extlib (>= 0.9.15)
- multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3)
execjs
json
@@ -83,7 +80,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
- bindata (2.3.5)
+ bindata (2.4.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
@@ -146,6 +143,8 @@ GEM
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
html-pipeline
+ declarative (0.0.10)
+ declarative-option (0.1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
@@ -167,9 +166,9 @@ GEM
docile (1.1.5)
domain_name (0.5.20161021)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.2.0)
+ doorkeeper (4.2.6)
railties (>= 4.2)
- doorkeeper-openid_connect (1.1.2)
+ doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
@@ -188,13 +187,12 @@ GEM
excon (0.57.1)
execjs (2.6.0)
expression_parser (0.9.0)
- extlib (0.9.16)
factory_girl (4.7.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
- faraday (0.12.1)
+ faraday (0.12.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
@@ -275,7 +273,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.33.0)
+ gitaly-proto (0.39.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -288,12 +286,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-grit (2.8.1)
+ gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
- mime-types (>= 1.16, < 3)
+ mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.5.1)
+ gitlab-markup (1.6.2)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -319,20 +317,16 @@ GEM
json
multi_json
request_store (>= 1.0)
- google-api-client (0.8.7)
- activesupport (>= 3.2, < 5.0)
- addressable (~> 2.3)
- autoparse (~> 0.3)
- extlib (~> 0.9)
- faraday (~> 0.9)
- googleauth (~> 0.3)
- launchy (~> 2.4)
- multi_json (~> 1.10)
- retriable (~> 1.4)
- signet (~> 0.6)
+ google-api-client (0.13.6)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (~> 0.5)
+ httpclient (>= 2.8.1, < 3.0)
+ mime-types (~> 3.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.0)
google-protobuf (3.4.0.2)
- googleauth (0.5.1)
- faraday (~> 0.9)
+ googleauth (0.5.3)
+ faraday (~> 0.12)
jwt (~> 1.4)
logging (~> 2.0)
memoist (~> 0.12)
@@ -355,9 +349,9 @@ GEM
activesupport
grape (>= 0.16.0)
rake
- grape_logging (1.6.0)
+ grape_logging (1.7.0)
grape
- grpc (1.4.5)
+ grpc (1.6.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
@@ -416,14 +410,14 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
- json-jwt (1.7.1)
+ json-jwt (1.7.2)
activesupport
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
- json-schema (2.6.2)
- addressable (~> 2.3.8)
+ json-schema (2.8.0)
+ addressable (>= 2.4)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
@@ -475,18 +469,20 @@ GEM
mail (2.6.6)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
- memoist (0.15.0)
+ memoist (0.16.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
- mime-types (2.99.3)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_mime (0.1.4)
- mini_portile2 (2.2.0)
+ mini_portile2 (2.3.0)
minitest (5.7.0)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
- multi_json (1.12.1)
+ multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (1.0.0)
@@ -496,8 +492,8 @@ GEM
net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0)
- nokogiri (1.8.0)
- mini_portile2 (~> 2.2.0)
+ nokogiri (1.8.1)
+ mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.4.0)
@@ -516,10 +512,10 @@ GEM
omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.3.1)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
- omniauth-azure-oauth2 (0.0.6)
+ omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0)
omniauth (~> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-oauth2 (~> 1.4)
omniauth-cas3 (1.1.4)
addressable (~> 2.3)
nokogiri (~> 1.7, >= 1.7.1)
@@ -545,7 +541,7 @@ GEM
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
- omniauth-oauth2 (1.3.1)
+ omniauth-oauth2 (1.4.0)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2)
@@ -635,6 +631,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
+ public_suffix (3.0.0)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.8)
rack-accept (0.4.5)
@@ -684,7 +681,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
- rake (12.0.0)
+ rake (12.1.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
@@ -717,6 +714,10 @@ GEM
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis (>= 2.2)
+ representable (3.0.4)
+ declarative (< 0.1.0)
+ declarative-option (< 0.2.0)
+ uber (< 0.2.0)
request_store (1.3.1)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
@@ -724,7 +725,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
- retriable (1.4.1)
+ retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.2.1)
@@ -882,6 +883,7 @@ GEM
ffi
sysexits (1.2.0)
temple (0.7.7)
+ test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
@@ -902,12 +904,13 @@ GEM
tzinfo (1.2.3)
thread_safe (~> 0.1)
u2f (0.2.1)
+ uber (0.1.0)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.2)
+ unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.1.0)
kgio (~> 2.6)
@@ -962,7 +965,7 @@ DEPENDENCIES
ace-rails-ap (~> 4.1.0)
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
- addressable (~> 2.3.8)
+ addressable (~> 2.5.2)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
@@ -999,7 +1002,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
- doorkeeper-openid_connect (~> 1.1.0)
+ doorkeeper-openid_connect (~> 1.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
@@ -1024,20 +1027,20 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.33.0)
+ gitaly-proto (~> 0.39.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab-markup (~> 1.5.1)
+ gitlab-markup (~> 1.6.2)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
- google-api-client (~> 0.8.6)
+ google-api-client (~> 0.13.6)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
- grape_logging (~> 1.6)
+ grape_logging (~> 1.7)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1050,7 +1053,7 @@ DEPENDENCIES
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
- json-schema (~> 2.6.2)
+ json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.11.0)
@@ -1067,14 +1070,14 @@ DEPENDENCIES
mysql2 (~> 0.4.5)
net-ldap
net-ssh (~> 4.1.0)
- nokogiri (~> 1.8.0)
+ nokogiri (~> 1.8.1)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
- omniauth-azure-oauth2 (~> 0.0.6)
+ omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
@@ -1163,6 +1166,7 @@ DEPENDENCIES
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
+ test-prof (~> 0.2.5)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
diff --git a/LICENSE b/LICENSE
index ad4f2872db5..15c423e1416 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,7 @@
Copyright (c) 2011-2017 GitLab B.V.
+With regard to the GitLab Software:
+
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
@@ -17,3 +19,7 @@ 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.
+
+For all third party components incorporated into the GitLab Software, those
+components are licensed under the original license provided by the owner of the
+applicable component. \ No newline at end of file
diff --git a/PROCESS.md b/PROCESS.md
index ed4e84dd0b6..5e65bb59246 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
+A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
+and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
+just like any other issue, to help GitLab team members focus on issues that are
+relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
+
## Release retrospective and kickoff
- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
diff --git a/VERSION b/VERSION
index ddadd9f9c5a..22dd5aa0686 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.6.0-pre
+10.1.0-pre
diff --git a/app/assets/images/auth_buttons/signin_with_google.png b/app/assets/images/auth_buttons/signin_with_google.png
new file mode 100644
index 00000000000..b1327b4f7b4
--- /dev/null
+++ b/app/assets/images/auth_buttons/signin_with_google.png
Binary files differ
diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico
index 156fcf07588..156fcf07588 100755..100644
--- a/app/assets/images/favicon-blue.ico
+++ b/app/assets/images/favicon-blue.ico
Binary files differ
diff --git a/app/assets/images/icon_image_comment.svg b/app/assets/images/icon_image_comment.svg
new file mode 100644
index 00000000000..cf6cb972940
--- /dev/null
+++ b/app/assets/images/icon_image_comment.svg
@@ -0,0 +1 @@
+<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icon_image_comment@2x.svg b/app/assets/images/icon_image_comment@2x.svg
new file mode 100644
index 00000000000..83be91d3705
--- /dev/null
+++ b/app/assets/images/icon_image_comment@2x.svg
@@ -0,0 +1 @@
+<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
new file mode 100644
index 00000000000..c0ed2ffdcb2
--- /dev/null
+++ b/app/assets/images/icons.json
@@ -0,0 +1 @@
+{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
new file mode 100644
index 00000000000..b9829d0d450
--- /dev/null
+++ b/app/assets/images/icons.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="ehfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="ehsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="ehthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/issues.svg b/app/assets/images/illustrations/issues.svg
new file mode 100644
index 00000000000..c8e0504732d
--- /dev/null
+++ b/app/assets/images/illustrations/issues.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.942" x="25" y="88.423" rx="2"/><mask id="h" width="25" height="8.942" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.801h43v61.603H16z"/><mask id="i" width="43" height="61.603" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.603l13.187 9.358c.449.32.876 1.015.955 1.568l3.575 24.863c.157 1.086-.253 1.257-.912.384L66 86.436l-9-6.955"/><mask id="j" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.25 60.603l13.186 9.358c.45.32.876 1.015.956 1.568l3.575 24.863c.156 1.086-.253 1.257-.912.384l-7.806-10.34-9-6.955"/><mask id="k" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.801L35.786 1.456c.947-1.357 2.48-1.36 3.428 0L59 29.8"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.265" height="35.509" x="6.367" rx="13.133"/><mask id="m" width="26.265" height="35.509" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.837" height="22.386" x="4.082" rx="8.418"/><mask id="n" width="16.837" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792 255)"><path d="M225.437 59.587c-.059.59-.132 1.27-.22 2.03a178.367 178.367 0 0 1-.965 7.07 1.5 1.5 0 1 0 2.963.465c.4-2.553.726-4.975.982-7.19a137.446 137.446 0 0 0 .297-2.832 1.5 1.5 0 1 0-2.989-.26c-.01.123-.033.365-.068.717zm-5.563 28.354a1.5 1.5 0 0 0 2.853.929c.975-2.997 1.849-6.283 2.628-9.797a1.5 1.5 0 1 0-2.928-.65c-.76 3.426-1.61 6.62-2.553 9.518zm-9.947 15.225a1.5 1.5 0 1 0 1.001 2.828c2.98-1.055 5.542-3.68 7.78-7.627a1.5 1.5 0 0 0-2.61-1.48c-1.915 3.378-3.995 5.508-6.171 6.279zm-19.488 4.417a1.5 1.5 0 1 0 1.164 2.765c3.12-1.314 6.272-2.324 9.258-2.981a1.5 1.5 0 1 0-.645-2.93c-3.167.697-6.491 1.763-9.777 3.146zm-17.208 11.043a1.5 1.5 0 0 0 2.066 2.175c2.282-2.169 4.866-4.162 7.676-5.946a1.5 1.5 0 0 0-1.608-2.533c-2.97 1.885-5.707 3.998-8.134 6.304zm-10.777 17.623a1.5 1.5 0 1 0 2.91.732c.768-3.054 2.041-5.977 3.78-8.748a1.5 1.5 0 0 0-2.54-1.595c-1.903 3.032-3.302 6.244-4.15 9.611zm-.265 20.444a1.5 1.5 0 1 0 2.977-.375c-.367-2.91-.58-6.137-.645-9.817a1.5 1.5 0 0 0-3 .053c.067 3.783.287 7.116.668 10.139zm6.219 19.472a1.5 1.5 0 0 0 2.652-1.403c-1.674-3.162-2.903-5.995-3.848-8.943a1.5 1.5 0 1 0-2.857.916c1.003 3.127 2.302 6.12 4.053 9.43zm7.566 12.77a595.837 595.837 0 0 1 2.73 4.475 1.5 1.5 0 0 0 2.569-1.551 626.463 626.463 0 0 0-2.744-4.495c.08.13-1.954-3.173-2.486-4.04a1.5 1.5 0 1 0-2.558 1.567c.534.87 2.571 4.178 2.489 4.045zm8.856 22.447a1.5 1.5 0 0 0 3-.039 32.214 32.214 0 0 0-1.837-10.326 1.5 1.5 0 0 0-2.828.999 29.212 29.212 0 0 1 1.665 9.366zm-5.483 18.028a1.5 1.5 0 0 0 2.497 1.662 36.203 36.203 0 0 0 4.488-9.416 1.5 1.5 0 0 0-2.868-.882 33.197 33.197 0 0 1-4.117 8.636z" fill="#FDE5D8"/><g transform="rotate(60 126.799 371.622)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.66m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.353" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="matrix(-1 0 0 1 18.25 0)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="34.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="40.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="46.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.433"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.465"/></g><path fill="#EEE" d="M96.043 37.21c-.152 1.688.081 3.816.997 6.147a1.016 1.016 0 0 0 1.89-.74c-.791-2.014-.99-3.832-.865-5.226.01-.114.02-.186.024-.211a1.015 1.015 0 1 0-2.002-.333 5.06 5.06 0 0 0-.044.363zm11.487 15.683c.491.24 1.098.063 1.355-.394.257-.456.068-1.02-.424-1.26-1.866-.907-3.458-1.914-4.794-3.007a1.058 1.058 0 0 0-1.417.085.888.888 0 0 0 .091 1.317c1.458 1.192 3.183 2.283 5.19 3.26zm13.131 6.06a1.032 1.032 0 0 0 1.293-.7 1.06 1.06 0 0 0-.686-1.32 376.355 376.355 0 0 1-5.915-1.882 1.031 1.031 0 0 0-1.303.681 1.06 1.06 0 0 0 .668 1.33c1.729.569 2.905.94 5.943 1.891zm11.934 3.928c.45.246 1.022.098 1.28-.33a.872.872 0 0 0-.346-1.221c-1.494-.819-3.192-1.545-5.267-2.275-.486-.17-1.025.067-1.204.53-.18.464.07.978.555 1.149 1.984.697 3.59 1.384 4.982 2.147zm9.382 10.502c.205.494.81.742 1.349.554.54-.188.81-.74.605-1.234-.85-2.048-1.853-3.796-3.037-5.305-.337-.429-.99-.527-1.459-.218-.469.308-.575.906-.238 1.335 1.074 1.368 1.992 2.97 2.78 4.868zm2.632 13.642c.018.553.568.99 1.228.975.66-.016 1.18-.477 1.163-1.03-.073-2.204-.27-4.206-.622-6.12-.101-.547-.712-.923-1.365-.838-.652.084-1.1.597-.999 1.144.336 1.825.525 3.745.595 5.869z"/><path fill="#E5E5E5" d="M144.142 95.73a244.285 244.285 0 0 0-.142 5.254c-.007.553.396 1.008.902 1.016.506.008.923-.433.93-.985.02-1.467.056-2.681.142-5.211l.026-.767c.018-.552-.377-1.016-.882-1.036-.506-.02-.931.41-.95.963l-.026.766zm.797 19.471c.12.545.673.892 1.236.777.562-.116.921-.651.802-1.196-.417-1.9-.71-3.84-.897-5.864-.052-.554-.558-.964-1.131-.914-.573.05-.996.54-.945 1.094.195 2.102.5 4.121.935 6.103zm5.056 12.324c.296.454.953.61 1.467.348.514-.261.69-.841.395-1.295a40.725 40.725 0 0 1-2.79-4.991c-.227-.485-.855-.715-1.403-.515-.548.2-.81.755-.582 1.239a42.56 42.56 0 0 0 2.913 5.214zm4.814 7.701a33.475 33.475 0 0 0 3.543 3.531 1.021 1.021 0 0 0 1.393-.066.908.908 0 0 0-.07-1.326 31.562 31.562 0 0 1-3.34-3.328 59.092 59.092 0 0 1-.576-.682 1.02 1.02 0 0 0-1.386-.152.909.909 0 0 0-.16 1.32c.196.234.394.469.596.703zm15.825 11.677c.48.242 1.052.017 1.276-.501.224-.52.016-1.136-.464-1.378a49.756 49.756 0 0 1-4.986-2.872c-.453-.298-1.044-.144-1.32.345-.276.488-.133 1.126.32 1.424a51.568 51.568 0 0 0 5.174 2.982z"/><path fill="#EEE" d="M184.733 151.97c.553.141 1.108-.226 1.239-.82.131-.595-.21-1.192-.763-1.333a72.17 72.17 0 0 1-5.863-1.763c-.54-.188-1.12.13-1.296.712-.175.581.121 1.205.662 1.393a74.018 74.018 0 0 0 6.021 1.81zm13.2 2.028c.554.04 1.03-.445 1.065-1.083.035-.639-.386-1.188-.939-1.228a71.842 71.842 0 0 1-5.92-.676c-.55-.086-1.055.358-1.13.991-.074.634.31 1.217.86 1.303a73.28 73.28 0 0 0 6.065.693zm14.188-1.392c.55-.055.94-.457.871-.9-.068-.441-.569-.755-1.118-.7-1.917.192-3.893.32-5.91.382-.554.017-.985.392-.963.837.021.445.487.792 1.04.774a88.939 88.939 0 0 0 6.08-.393zm14.245-2.657c.53-.22.776-.816.55-1.332a1.053 1.053 0 0 0-1.367-.535 44.421 44.421 0 0 1-5.777 1.923 1.012 1.012 0 0 0-.736 1.243c.15.542.721.863 1.277.717a46.532 46.532 0 0 0 6.054-2.016zm11.483-9.532c.292-.435.148-1.006-.32-1.277-.47-.27-1.087-.138-1.379.297-.957 1.424-2.225 2.734-3.784 3.92a.88.88 0 0 0-.138 1.304c.35.396.98.453 1.408.128 1.723-1.31 3.136-2.771 4.213-4.372zm7.824-9.73a.965.965 0 0 0 .09-1.358.958.958 0 0 0-1.355-.09 44.935 44.935 0 0 0-4.17 4.163.965.965 0 0 0 .089 1.359.957.957 0 0 0 1.354-.089 43.05 43.05 0 0 1 3.991-3.985zm11.808-7.817c.476-.257.657-.858.405-1.342a.967.967 0 0 0-1.319-.412 67.097 67.097 0 0 0-5.123 3.059c-.451.298-.58.913-.287 1.373.294.46.898.59 1.35.292a65.257 65.257 0 0 1 4.974-2.97zm12.795-5.948c.55-.169.851-.724.672-1.241-.179-.518-.77-.8-1.32-.632a92.308 92.308 0 0 0-5.975 2.054c-.536.205-.794.78-.576 1.283.218.504.83.746 1.366.541a90.115 90.115 0 0 1 5.833-2.005z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.06 56s-17.47 33-12 53c5.47 20 17 32 38 44s32.44-5 60.94 6 29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108 173)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g fill="#FC8A51" transform="rotate(15 101.633 923.121)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g fill="#FC8A51" transform="scale(-1 1) rotate(-15 -102.031 920.099)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g transform="rotate(15 71.738 842.306)"><g fill="#FC8A51" transform="translate(29.449 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><g fill="#FC8A51" transform="matrix(-1 0 0 1 9.551 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.163 12.912h23.878v3H7.163z"/></g><g fill="#EEE" transform="scale(-1 1) rotate(15 -60.75 -335.206)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g fill="#EEE" transform="rotate(-15 60.494 -337.144)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g transform="scale(-1 1) rotate(15 -79.491 -386.955)"><g fill="#EEE" transform="translate(18.878 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><g fill="#EEE" transform="matrix(-1 0 0 1 6.122 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.592 8.14h15.306v2H4.592z"/></g><g fill="#FFF" transform="translate(0 103)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g transform="translate(39 142)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.732 13.475l-1.766-1.767a1.5 1.5 0 1 0-2.122 2.122l2.826 2.826h.001v.001c.59.59 1.535.587 2.119.003l6.37-6.37a1.504 1.504 0 0 0-.003-2.118 1.494 1.494 0 0 0-2.118-.004l-5.307 5.307z"/></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.704-7.746 11.013-20.925 4.862-31.578-7.02-12.16-22.405-16.422-34.362-9.518-11.958 6.904-15.96 22.358-8.939 34.518 6.236 10.8 19.068 15.37 30.238 11.42l10.899 18.879a4.765 4.765 0 0 0 6.508 1.748 4.768 4.768 0 0 0 1.74-6.51l-10.946-18.959zm-8.434-4.609c7.857-4.536 10.487-14.692 5.873-22.683-4.613-7.991-14.723-10.791-22.58-6.255-7.858 4.537-10.488 14.693-5.875 22.684 4.614 7.99 14.724 10.791 22.582 6.254z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/labels.svg b/app/assets/images/illustrations/labels.svg
new file mode 100644
index 00000000000..3a2d521323b
--- /dev/null
+++ b/app/assets/images/illustrations/labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zm-8 0c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zm344-121l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zm9.2 164.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/merge_requests.svg b/app/assets/images/illustrations/merge_requests.svg
new file mode 100644
index 00000000000..b9b8f0058e6
--- /dev/null
+++ b/app/assets/images/illustrations/merge_requests.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/monitoring/_getting_started.svg b/app/assets/images/illustrations/monitoring/getting_started.svg
index db7a1c2e708..db7a1c2e708 100644
--- a/app/views/shared/empty_states/monitoring/_getting_started.svg
+++ b/app/assets/images/illustrations/monitoring/getting_started.svg
diff --git a/app/views/shared/empty_states/monitoring/_loading.svg b/app/assets/images/illustrations/monitoring/loading.svg
index 6bbd7a6c5b9..6bbd7a6c5b9 100644
--- a/app/views/shared/empty_states/monitoring/_loading.svg
+++ b/app/assets/images/illustrations/monitoring/loading.svg
diff --git a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
index 62537d87d5d..62537d87d5d 100644
--- a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg
+++ b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
diff --git a/app/assets/images/illustrations/pipelines_empty.svg b/app/assets/images/illustrations/pipelines_empty.svg
new file mode 100644
index 00000000000..f3107c8f062
--- /dev/null
+++ b/app/assets/images/illustrations/pipelines_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g fill="#e5e5e5" transform="translate(0 102)"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="M152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 152 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 166 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 180 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 194 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 208 23"/></g><g fill="#31af64"><path fill-rule="nonzero" d="M19 144c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="M17.07 127.02l-2.829-2.829a1.995 1.995 0 0 0-2.828 0 1.995 1.995 0 0 0 0 2.828l4.243 4.243a1.995 1.995 0 0 0 2.822.006l7.79-7.79a1.997 1.997 0 0 0-.006-2.823 1.992 1.992 0 0 0-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a"><path fill-rule="nonzero" d="M126 149.5c-12.979 0-23.5-10.521-23.5-23.5s10.521-23.5 23.5-23.5 23.5 10.521 23.5 23.5-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5s-8.283-18.5-18.5-18.5-18.5 8.283-18.5 18.5 8.283 18.5 18.5 18.5"/><path d="M130.24 126l2.833-2.833a3 3 0 0 0-4.243-4.243l-2.833 2.833-2.833-2.833a3 3 0 0 0-4.243 4.243l2.833 2.833-2.833 2.833a3 3 0 0 0 4.243 4.243l2.833-2.833 2.833 2.833a3 3 0 0 0 4.243-4.243L130.24 126"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M236 139c-7.732 0-14-6.268-14-14s6.268-14 14-14 14 6.268 14 14-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10"/><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="M64.82 76H98c4.419 0 8-3.579 8-7.99V7.99C106 3.577 102.417 0 98 0H8.009c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855a4.357 4.357 0 0 0 6.354 0L64.824 76"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="M151.869 77.403c-13.26 9.264-31.649 7.977-43.484-3.858-13.279-13.279-13.279-34.806 0-48.084 13.278-13.278 34.805-13.278 48.083 0 11.836 11.836 13.118 30.23 3.858 43.485.133.111.262.229.387.354l15.556 15.555a6.004 6.004 0 0 1 0 8.486 5.997 5.997 0 0 1-8.486 0l-15.555-15.556a6.051 6.051 0 0 1-.355-.387m-1.06-9.512c10.154-10.154 10.154-26.617 0-36.77-10.153-10.154-26.616-10.154-36.77 0-10.153 10.153-10.153 26.616 0 36.77 10.154 10.153 26.617 10.153 36.77 0"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/pipelines_failed.svg b/app/assets/images/illustrations/pipelines_failed.svg
new file mode 100644
index 00000000000..8daf7da86ed
--- /dev/null
+++ b/app/assets/images/illustrations/pipelines_failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12a36.87 36.87 0 0 0-15.32 3.311c-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088C145.95 45.083 134 59.645 134 76.996c0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="b" cx="41" cy="41" rx="41" ry="41"/><mask id="c" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="d" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd"><path stroke="#b5a7dd" stroke-width="4" d="M228.415 137.792c8.443 17.156 21.89 32.082 39.688 42.358"/><path fill="#fb722e" d="M284.464 183.822a2.006 2.006 0 0 1 2.74-.727l6.914 3.992a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.992a2.001 2.001 0 0 1-.74-2.737m-5 8.66a2.006 2.006 0 0 1 2.74-.726l6.913 3.991a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.991a2.001 2.001 0 0 1-.74-2.737"/><path fill="#fde5d8" fill-rule="nonzero" d="M267.072 189.947l5.196 3a5.998 5.998 0 0 0 8.195-2.194l3.005-5.205a5.995 5.995 0 0 0-2.198-8.193l-5.196-3-9 15.588m6.032-18.447a3.005 3.005 0 0 1 4.098-1.11l6.07 3.505c4.784 2.761 6.426 8.871 3.662 13.658l-3.005 5.204c-2.76 4.782-8.875 6.42-13.659 3.658l-6.07-3.505a2.999 2.999 0 0 1-1.088-4.104l9.992-17.306"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M260.597 18.747C266.208 9.657 276.116 4 287 4c17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466A34.855 34.855 0 0 0 322 35.001c0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><path fill="#b5a7dd" d="M281.982 23.991l-2.526 1.154-2.992-2.993a.4.4 0 0 0-.564.009l-1.738 1.738a.392.392 0 0 0-.009.564l2.987 2.987-1.147 2.524a12.26 12.26 0 0 0-1.04 3.883l-.269 2.76-4.08 1.093a.399.399 0 0 0-.275.492l.636 2.375c.06.223.273.346.485.29l4.087-1.096 1.611 2.262a12.017 12.017 0 0 0 2.827 2.828l2.26 1.612-1.094 4.08a.399.399 0 0 0 .29.485l2.374.636a.393.393 0 0 0 .493-.275l1.093-4.08 2.763-.267a12.14 12.14 0 0 0 3.862-1.035l2.526-1.154 2.992 2.992a.4.4 0 0 0 .564-.008l1.738-1.738a.392.392 0 0 0 .009-.564l-2.987-2.987 1.147-2.524a12.26 12.26 0 0 0 1.04-3.883l.27-2.76 4.08-1.093a.399.399 0 0 0 .274-.493l-.636-2.374a.393.393 0 0 0-.485-.29l-4.087 1.096-1.611-2.262a12.017 12.017 0 0 0-2.826-2.828l-2.26-1.612 1.093-4.08a.399.399 0 0 0-.29-.485l-2.373-.636a.393.393 0 0 0-.493.274l-1.094 4.081-2.763.266c-1.336.129-2.64.48-3.862 1.036m3.48-5.02l.375-1.4a4.393 4.393 0 0 1 5.392-3.103l2.375.636a4.399 4.399 0 0 1 3.117 5.383l-.375 1.401a16.077 16.077 0 0 1 3.761 3.767l1.405-.376a4.397 4.397 0 0 1 5.386 3.118l.636 2.375a4.398 4.398 0 0 1-3.103 5.39l-1.402.376a16.217 16.217 0 0 1-1.378 5.143l1.027 1.026a4.392 4.392 0 0 1-.008 6.22l-1.739 1.738a4.4 4.4 0 0 1-6.224.008l-1.028-1.028a16.09 16.09 0 0 1-5.14 1.381l-.376 1.4a4.393 4.393 0 0 1-5.392 3.104l-2.374-.636a4.399 4.399 0 0 1-3.118-5.383l.376-1.401a16.077 16.077 0 0 1-3.762-3.767l-1.404.376a4.397 4.397 0 0 1-5.386-3.118l-.637-2.374a4.398 4.398 0 0 1 3.103-5.391l1.402-.376a16.217 16.217 0 0 1 1.378-5.143l-1.026-1.026a4.392 4.392 0 0 1 .008-6.22l1.738-1.738a4.4 4.4 0 0 1 6.224-.008l1.028 1.028a16.09 16.09 0 0 1 5.141-1.381"/><path fill="#6b4fbb" d="M286.367 37.355a2.439 2.439 0 1 0 1.262-4.711 2.439 2.439 0 0 0-1.262 4.711m-1.035 3.864a6.44 6.44 0 1 1 3.333-12.44 6.44 6.44 0 0 1-3.333 12.44"/></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#c)" stroke-linejoin="round" xlink:href="#a"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#d)" xlink:href="#b"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M41 78c20.435 0 37-16.565 37-37S61.435 4 41 4 4 20.565 4 41s16.565 37 37 37m0 4C18.356 82 0 63.644 0 41S18.356 0 41 0s41 18.356 41 41-18.356 41-41 41"/><path fill="#b5a7dd" d="M34.363 26.44l-2.527 1.154-3.211-3.211a1.495 1.495 0 0 0-2.117-.005l-2.131 2.13a1.504 1.504 0 0 0 .005 2.117l3.206 3.206-1.147 2.524a16.09 16.09 0 0 0-.897 2.503 16.08 16.08 0 0 0-.475 2.616l-.269 2.76-4.379 1.174a1.495 1.495 0 0 0-1.063 1.83l.78 2.911a1.504 1.504 0 0 0 1.836 1.054l4.387-1.176 1.612 2.263a15.954 15.954 0 0 0 3.737 3.742l2.26 1.612-1.173 4.38a1.495 1.495 0 0 0 1.053 1.835l2.908.78a1.504 1.504 0 0 0 1.83-1.063l1.174-4.38 2.763-.266a15.977 15.977 0 0 0 5.108-1.372l2.527-1.154 3.211 3.212a1.495 1.495 0 0 0 2.117.005l2.131-2.131a1.504 1.504 0 0 0-.005-2.117l-3.206-3.206 1.147-2.524a16.09 16.09 0 0 0 .897-2.503 16.1 16.1 0 0 0 .475-2.616l.269-2.76 4.379-1.173a1.495 1.495 0 0 0 1.063-1.83l-.78-2.912a1.504 1.504 0 0 0-1.836-1.054l-4.387 1.176-1.612-2.262a15.954 15.954 0 0 0-3.737-3.743l-2.26-1.612 1.173-4.38a1.495 1.495 0 0 0-1.053-1.835l-2.908-.779a1.504 1.504 0 0 0-1.83 1.063l-1.174 4.38-2.763.265c-1.767.17-3.493.636-5.108 1.373m4.726-5.355l.455-1.699a5.504 5.504 0 0 1 6.73-3.89l2.907.778a5.495 5.495 0 0 1 3.882 6.735l-.455 1.699a19.95 19.95 0 0 1 4.673 4.68l1.704-.457a5.503 5.503 0 0 1 6.734 3.886l.78 2.91a5.493 5.493 0 0 1-3.894 6.73l-1.701.455a20.134 20.134 0 0 1-.593 3.265 20.134 20.134 0 0 1-1.119 3.124l1.245 1.246a5.507 5.507 0 0 1 .008 7.774l-2.13 2.13a5.5 5.5 0 0 1-7.775-.001l-1.248-1.248c-2 .914-4.157 1.502-6.387 1.717l-.455 1.699a5.504 5.504 0 0 1-6.73 3.89l-2.907-.778a5.495 5.495 0 0 1-3.882-6.735l.455-1.699a19.95 19.95 0 0 1-4.673-4.68l-1.704.457a5.503 5.503 0 0 1-6.734-3.886l-.78-2.91a5.493 5.493 0 0 1 3.894-6.73l1.701-.455a20.258 20.258 0 0 1 1.712-6.389l-1.245-1.246a5.507 5.507 0 0 1-.008-7.774l2.13-2.13a5.5 5.5 0 0 1 7.775.001l1.248 1.248c2-.914 4.157-1.502 6.387-1.717"/><path fill="#6b4fbb" d="M39.965 44.863a4 4 0 1 0 2.07-7.727 4 4 0 0 0-2.07 7.727m-1.036 3.864a8 8 0 1 1 4.142-15.455 8 8 0 0 1-4.142 15.455"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="M144 169.541v30.01a4.002 4.002 0 0 0 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01a4.002 4.002 0 0 0-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e" transform="translate(140 161)"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364a2 2 0 1 0 2.3 3.272c15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617a2.001 2.001 0 0 0 2.29-3.28"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/priority_labels.svg b/app/assets/images/illustrations/priority_labels.svg
new file mode 100644
index 00000000000..b79c551d3d7
--- /dev/null
+++ b/app/assets/images/illustrations/priority_labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zm5-34C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/todos_all_done.svg b/app/assets/images/illustrations/todos_all_done.svg
new file mode 100644
index 00000000000..6387497a6fb
--- /dev/null
+++ b/app/assets/images/illustrations/todos_all_done.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/todos_empty.svg b/app/assets/images/illustrations/todos_empty.svg
new file mode 100644
index 00000000000..4de6cb403b9
--- /dev/null
+++ b/app/assets/images/illustrations/todos_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/><mask id="l" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><rect id="b" width="180" height="220" rx="6"/><mask id="m" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><rect id="c" width="28" height="28" rx="4"/><mask id="n" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><rect id="d" width="28" height="28" rx="4"/><mask id="o" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><circle id="e" cx="21.5" cy="21.5" r="21.5"/><mask id="p" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><circle id="f" cx="26.5" cy="26.5" r="26.5"/><mask id="q" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><circle id="g" cx="9.5" cy="4.5" r="4.5"/><mask id="r" width="13" height="13" x="-2" y="-2"><path fill="#fff" d="M3-2h13v13H3z"/><use xlink:href="#g"/></mask><circle id="h" cx="26.5" cy="26.5" r="26.5"/><mask id="s" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><circle id="i" cx="21.5" cy="21.5" r="21.5"/><mask id="t" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/><mask id="u" width="52" height="44" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><circle id="k" cx="18.5" cy="18.5" r="18.5"/><mask id="v" width="37" height="37" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"><use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/><g transform="rotate(5 -707.333 618.042)"><use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/><g transform="translate(29 24)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(29 69)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(28 160)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/><rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/></g><g transform="translate(28 116)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/><rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/></g></g><g transform="rotate(-15 601.917 -782.362)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/><text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"><tspan x="12" y="27">@</tspan></text></g><g transform="rotate(15 -686.59 1035.907)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/><path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/><g transform="translate(17 14)"><use fill="#FC6D26" xlink:href="#g"/><use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/></g></g><g transform="rotate(15 -85.125 65.185)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/><path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/></g><g transform="rotate(-15 716.492 78.873)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/><path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/></g><g transform="rotate(-15 129.114 -585.74)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/><circle cx="16" cy="20" r="2" fill="#FC6D26"/><circle cx="27" cy="20" r="2" fill="#FC6D26"/><circle cx="38" cy="20" r="2" fill="#FC6D26"/></g><g transform="rotate(-15 1254.8 -458.986)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/><path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/><rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/new_nav.png b/app/assets/images/new_nav.png
deleted file mode 100644
index f98ca15d787..00000000000
--- a/app/assets/images/new_nav.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/old_nav.png b/app/assets/images/old_nav.png
deleted file mode 100644
index 23fae7aa19e..00000000000
--- a/app/assets/images/old_nav.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 346de4ad11e..3de192d56eb 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,7 +1,7 @@
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-class AbuseReports {
+export default class AbuseReports {
constructor() {
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
$(document)
@@ -32,6 +32,3 @@ class AbuseReports {
}
}
}
-
-window.gl = window.gl || {};
-window.gl.AbuseReports = AbuseReports;
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index 8f5e2e545ec..2bc77859c26 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -1,4 +1,4 @@
-class AjaxLoadingSpinner {
+export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
@@ -30,6 +30,3 @@ class AjaxLoadingSpinner {
classList.toggle('fa-spin');
}
}
-
-window.gl = window.gl || {};
-gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 22fa1f2a609..4f01345ee3b 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,6 +2,7 @@
/* global Flash */
import _ from 'underscore';
import Cookies from 'js-cookie';
+import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@@ -23,6 +24,9 @@ const categoryLabelMap = {
flags: 'Flags',
};
+const IS_VISIBLE = 'is-visible';
+const IS_RENDERED = 'is-rendered';
+
class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
@@ -50,7 +54,7 @@ class AwardsHandler {
if (!$target.closest('.emoji-menu').length) {
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
- $('.emoji-menu').removeClass('is-visible');
+ this.hideMenuElement($('.emoji-menu'));
}
}
});
@@ -87,12 +91,12 @@ class AwardsHandler {
if ($menu.length) {
if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active');
- $menu.removeClass('is-visible');
+ this.hideMenuElement($menu);
$('.js-emoji-menu-search').blur();
} else {
$addBtn.addClass('is-active');
this.positionMenu($menu, $addBtn);
- $menu.addClass('is-visible');
+ this.showMenuElement($menu);
$('.js-emoji-menu-search').focus();
}
} else {
@@ -102,7 +106,7 @@ class AwardsHandler {
$addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
- $createdMenu.addClass('is-visible');
+ this.showMenuElement($createdMenu);
$('.js-emoji-menu-search').focus();
}, 200);
});
@@ -237,10 +241,11 @@ class AwardsHandler {
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
- if (gl.utils.isInIssuePage() && !isMainAwardsBlock) {
+ if (isInIssuePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
- $('.emoji-menu').removeClass('is-visible');
+ this.hideMenuElement($('.emoji-menu'));
+
$('.js-add-award.is-active').removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
@@ -260,7 +265,8 @@ class AwardsHandler {
return typeof callback === 'function' ? callback() : undefined;
});
- $('.emoji-menu').removeClass('is-visible');
+ this.hideMenuElement($('.emoji-menu'));
+
return $('.js-add-award.is-active').removeClass('is-active');
}
@@ -288,7 +294,7 @@ class AwardsHandler {
}
getVotesBlock() {
- if (gl.utils.isInIssuePage()) {
+ if (isInIssuePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
@@ -452,11 +458,11 @@ class AwardsHandler {
userAuthored($emojiButton) {
const oldTitle = this.getAwardTooltip($emojiButton);
const newTitle = 'You cannot vote on your own issue, MR and note';
- gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
+ updateTooltipTitle($emojiButton, newTitle).tooltip('show');
// Restore tooltip back to award list
return setTimeout(() => {
$emojiButton.tooltip('hide');
- gl.utils.updateTooltipTitle($emojiButton, oldTitle);
+ updateTooltipTitle($emojiButton, oldTitle);
}, 2800);
}
@@ -528,6 +534,33 @@ class AwardsHandler {
return $matchingElements.closest('li').clone();
}
+ /* showMenuElement and hideMenuElement are performance optimizations. We use
+ * opacity to show/hide the emoji menu, because we can animate it. But opacity
+ * leaves hidden elements in the render tree, which is unacceptable given the number
+ * of emoji elements in the emoji menu (5k+). To get the best of both worlds, we separately
+ * apply IS_RENDERED to add/remove the menu from the render tree and IS_VISIBLE to animate
+ * the menu being opened and closed. */
+
+ showMenuElement($emojiMenu) {
+ $emojiMenu.addClass(IS_RENDERED);
+
+ // enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added
+ return Promise.resolve()
+ .then(() => $emojiMenu.addClass(IS_VISIBLE));
+ }
+
+ hideMenuElement($emojiMenu) {
+ $emojiMenu.on(transitionEndEventString, (e) => {
+ if (e.currentTarget === e.target) {
+ $emojiMenu
+ .removeClass(IS_RENDERED)
+ .off(transitionEndEventString);
+ }
+ });
+
+ $emojiMenu.removeClass(IS_VISIBLE);
+ }
+
destroy() {
this.eventListeners.forEach((entry) => {
entry.element.off.call(entry.element, ...entry.args);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 79702c54852..2cf8f4fa935 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,5 @@
import '../commons/bootstrap';
+import { isInIssuePage } from '../lib/utils/common_utils';
// Quick Submit behavior
//
@@ -45,7 +46,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
- if (!gl.utils.isInIssuePage()) {
+ if (!isInIssuePage()) {
$submitButton.disable();
}
}
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 26d3419a162..ddd1fea3aca 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -3,6 +3,7 @@
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
+import csrf from '../lib/utils/csrf';
function toggleLoading($el, $icon, loading) {
if (loading) {
@@ -36,9 +37,7 @@ export default class BlobFileDropzone {
maxFiles: 1,
addRemoveLinks: true,
previewsContainer: '.dropzone-previews',
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
- },
+ headers: csrf.headers,
init: function () {
this.on('addedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index 27312d718b0..c858a6bb7b4 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -40,10 +40,10 @@ export default () => {
class="text-center"
v-if="error">
<span v-if="loadError">
- An error occured whilst loading the file. Please try again later.
+ An error occurred whilst loading the file. Please try again later.
</span>
<span v-else>
- An error occured whilst parsing the file.
+ An error occurred whilst parsing the file.
</span>
</p>
</div>
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 0ed915c1ac9..7109f356540 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -48,10 +48,10 @@ export default () => {
class="text-center"
v-if="error">
<span v-if="loadError">
- An error occured whilst loading the file. Please try again later.
+ An error occurred whilst loading the file. Please try again later.
</span>
<span v-else>
- An error occured whilst decoding the file.
+ An error occurred whilst decoding the file.
</span>
</p>
</div>
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 187fab084fd..e0b73f13d36 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,4 +1,6 @@
/* global Flash */
+import { handleLocationHash } from '../../lib/utils/common_utils';
+
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
@@ -114,7 +116,7 @@ export default class BlobViewer {
$(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line');
- gl.utils.handleLocationHash();
+ handleLocationHash();
this.toggleCopyButtonState();
})
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index ea00efe4b46..815248f38ee 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -77,9 +77,6 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- this.filterManager = new FilteredSearchBoards(Store.filter, true);
- this.filterManager.setup();
-
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
},
@@ -87,6 +84,9 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens);
},
mounted () {
+ this.filterManager = new FilteredSearchBoards(Store.filter, true);
+ this.filterManager.setup();
+
Store.disabled = this.disabled;
gl.boardService.all()
.then(response => response.json())
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
index 541b8049855..bc28f7f45f4 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -68,7 +68,7 @@ export default {
<div class="flash-container"
v-if="error">
<div class="flash-alert">
- An error occured. Please try again.
+ An error occurred. Please try again.
</div>
</div>
<label class="label-light"
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 13569df0c20..e571b11a83d 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -8,11 +8,11 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
return ModalStore.store;
},
props: {
- image: {
+ newIssuePath: {
type: String,
required: true,
},
- newIssuePath: {
+ emptyStateSvg: {
type: String,
required: true,
},
@@ -42,7 +42,7 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
<section class="empty-state">
<div class="row">
<div class="col-xs-12 col-sm-6 col-sm-push-6">
- <aside class="svg-content" v-html="image"></aside>
+ <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
<div class="col-xs-12 col-sm-6 col-sm-pull-6">
<div class="text-content">
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 96af69e7a36..d2044f20ebe 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -12,11 +12,11 @@ const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModal = Vue.extend({
props: {
- blankStateImage: {
+ newIssuePath: {
type: String,
required: true,
},
- newIssuePath: {
+ emptyStateSvg: {
type: String,
required: true,
},
@@ -150,14 +150,14 @@ gl.issueBoards.IssuesModal = Vue.extend({
:label-path="labelPath">
</modal-header>
<modal-list
- :image="blankStateImage"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
+ :empty-state-svg="emptyStateSvg"
v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
- :image="blankStateImage"
- :new-issue-path="newIssuePath"></empty-state>
+ :new-issue-path="newIssuePath"
+ :empty-state-svg="emptyStateSvg"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading || filterLoading">
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index b4a45feee4d..7c62134b3a3 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -15,7 +15,7 @@ gl.issueBoards.ModalList = Vue.extend({
type: String,
required: true,
},
- image: {
+ emptyStateSvg: {
type: String,
required: true,
},
@@ -119,8 +119,8 @@ gl.issueBoards.ModalList = Vue.extend({
class="empty-state add-issues-empty-state-filter text-center"
v-if="issuesCount > 0 && issues.length === 0">
<div
- class="svg-content"
- v-html="image">
+ class="svg-content">
+ <img :src="emptyStateSvg"/>
</div>
<div class="text-content">
<h4>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index d7f203b3f96..c19c989680d 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,6 +1,7 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
+/* eslint-disable func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
import _ from 'underscore';
+import CreateLabelDropdown from '../../create_label';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => {
label: {
id: label.id,
title: label.title,
- color: label.color
- }
+ color: label.color,
+ },
});
});
gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+ new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
@@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => {
const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
- href: '#'
+ href: '#',
});
const $labelColor = $('<span />', {
class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
+ style: `background-color: ${label.color}`,
});
return $li.append($a.prepend($labelColor));
},
search: {
- fields: ['title']
+ fields: ['title'],
},
filterable: true,
selectable: true,
@@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => {
label: {
id: label.id,
title: label.title,
- color: label.color
- }
+ color: label.color,
+ },
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
- }
+ },
});
});
};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 43928e602d6..ea82958e80d 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -2,6 +2,7 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
+import { getUrlParamsArray } from '../../lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -21,7 +22,7 @@ gl.issueBoards.BoardsStore = {
},
create () {
this.state.lists = [];
- this.filter.path = gl.utils.getUrlParamsArray().join('&');
+ this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} };
},
addList (listObj, defaultAvatar) {
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index af8bcdc1794..cbc28374b80 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -7,6 +7,7 @@ class DeleteModal {
this.$branchName = $('.js-branch-name', this.$modal);
this.$confirmInput = $('.js-delete-branch-input', this.$modal);
this.$deleteBtn = $('.js-delete-branch', this.$modal);
+ this.$notMerged = $('.js-not-merged', this.$modal);
this.bindEvents();
}
@@ -16,8 +17,10 @@ class DeleteModal {
}
setModalData(e) {
- this.branchName = e.currentTarget.dataset.branchName || '';
- this.deletePath = e.currentTarget.dataset.deletePath || '';
+ const branchData = e.currentTarget.dataset;
+ this.branchName = branchData.branchName || '';
+ this.deletePath = branchData.deletePath || '';
+ this.isMerged = !!branchData.isMerged;
this.updateModal();
}
@@ -30,6 +33,7 @@ class DeleteModal {
this.$confirmInput.val('');
this.$deleteBtn.attr('href', this.deletePath);
this.$deleteBtn.attr('disabled', true);
+ this.$notMerged.toggleClass('hidden', this.isMerged);
}
}
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index ae1a23132a7..3d27a3544eb 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -3,6 +3,7 @@ consistent-return, prefer-rest-params */
import _ from 'underscore';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
+import { setCiStatusFavicon } from './lib/utils/common_utils';
window.Build = (function () {
Build.timeout = null;
@@ -166,10 +167,10 @@ window.Build = (function () {
Build.prototype.getBuildTrace = function () {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
- data: this.state,
+ data: { state: this.state },
})
.done((log) => {
- gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
+ setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) {
this.state = log.state;
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index bd479700fd3..19388f1f9ae 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,9 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
+import { visitUrl } from './lib/utils/url_utility';
+import { convertPermissionToBoolean } from './lib/utils/common_utils';
window.BuildArtifacts = (function() {
function BuildArtifacts() {
this.disablePropagation();
this.setupEntryClick();
+ this.setupTooltips();
}
BuildArtifacts.prototype.disablePropagation = function() {
@@ -17,9 +20,28 @@ window.BuildArtifacts = (function() {
BuildArtifacts.prototype.setupEntryClick = function() {
return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
- return window.location = this.dataset.link;
+ visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
};
+ BuildArtifacts.prototype.setupTooltips = function() {
+ $('.js-artifact-tree-tooltip').tooltip({
+ placement: 'bottom',
+ // Stop the tooltip from hiding when we stop hovering the element directly
+ // We handle all the showing/hiding below
+ trigger: 'manual',
+ });
+
+ // We want the tooltip to show if you hover anywhere on the row
+ // But be placed below and in the middle of the file name
+ $('.js-artifact-tree-row')
+ .on('mouseenter', (e) => {
+ $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
+ })
+ .on('mouseleave', (e) => {
+ $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
+ });
+ };
+
return BuildArtifacts;
})();
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
new file mode 100644
index 00000000000..50dbeb06362
--- /dev/null
+++ b/app/assets/javascripts/clusters.js
@@ -0,0 +1,112 @@
+/* globals Flash */
+import Visibility from 'visibilityjs';
+import axios from 'axios';
+import Poll from './lib/utils/poll';
+import { s__ } from './locale';
+import './flash';
+
+/**
+ * Cluster page has 2 separate parts:
+ * Toggle button
+ *
+ * - Polling status while creating or scheduled
+ * -- Update status area with the response result
+ */
+
+class ClusterService {
+ constructor(options = {}) {
+ this.options = options;
+ }
+ fetchData() {
+ return axios.get(this.options.endpoint);
+ }
+}
+
+export default class Clusters {
+ constructor() {
+ const dataset = document.querySelector('.js-edit-cluster-form').dataset;
+
+ this.state = {
+ statusPath: dataset.statusPath,
+ clusterStatus: dataset.clusterStatus,
+ clusterStatusReason: dataset.clusterStatusReason,
+ toggleStatus: dataset.toggleStatus,
+ };
+
+ this.service = new ClusterService({ endpoint: this.state.statusPath });
+ this.toggleButton = document.querySelector('.js-toggle-cluster');
+ this.toggleInput = document.querySelector('.js-toggle-input');
+ this.errorContainer = document.querySelector('.js-cluster-error');
+ this.successContainer = document.querySelector('.js-cluster-success');
+ this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
+
+ this.toggleButton.addEventListener('click', this.toggle.bind(this));
+
+ if (this.state.clusterStatus !== 'created') {
+ this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
+ }
+
+ if (this.state.statusPath) {
+ this.initPolling();
+ }
+ }
+
+ toggle() {
+ this.toggleButton.classList.toggle('checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: (data) => {
+ const { status, status_reason } = data.data;
+ this.updateContainer(status, status_reason);
+ },
+ errorCallback: () => {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service.fetchData();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ hideAll() {
+ this.errorContainer.classList.add('hidden');
+ this.successContainer.classList.add('hidden');
+ this.creatingContainer.classList.add('hidden');
+ }
+
+ updateContainer(status, error) {
+ this.hideAll();
+ switch (status) {
+ case 'created':
+ this.successContainer.classList.remove('hidden');
+ break;
+ case 'errored':
+ this.errorContainer.classList.remove('hidden');
+ this.errorReasonContainer.textContent = error;
+ break;
+ case 'scheduled':
+ case 'creating':
+ this.creatingContainer.classList.remove('hidden');
+ break;
+ default:
+ this.hideAll();
+ }
+ }
+}
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
deleted file mode 100644
index 5f637524e30..00000000000
--- a/app/assets/javascripts/commit.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-/* global CommitFile */
-
-window.Commit = (function() {
- function Commit() {
- $('.files .diff-file').each(function() {
- return new CommitFile(this);
- });
- }
-
- return Commit;
-})();
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
deleted file mode 100644
index ee087c978dd..00000000000
--- a/app/assets/javascripts/commit/file.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
-/* global ImageFile */
-
-(function() {
- this.CommitFile = (function() {
- function CommitFile(file) {
- if ($('.image', file).length) {
- new gl.ImageFile(file);
- }
- }
-
- return CommitFile;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 17d14dc1e79..e7adf8814b8 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
+import 'vendor/jquery.waitforimages';
+
(function() {
gl.ImageFile = (function() {
var prepareFrames;
@@ -11,14 +13,17 @@
function ImageFile(file) {
this.file = file;
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
- // Determine if old and new file has same dimensions, if not show 'two-up' view
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
- if (width === deletedWidth && height === deletedHeight) {
- return _this.initViewModes();
- } else {
- return _this.initView('two-up');
- }
+ _this.initViewModes();
+
+ // Load two-up view after images are loaded
+ // so that we can display the correct width and height information
+ const $images = $('.two-up.view img', _this.file);
+
+ $images.waitForImages(function() {
+ _this.initView('two-up');
+ });
});
};
})(this));
@@ -134,8 +139,9 @@
width: maxWidth + 1,
height: maxHeight + 2
});
+ // Set swipeBar left position to match image frame
$swipeBar.css({
- left: 0
+ left: 1
});
wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 16c5d0fa344..1f9153d95bd 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -35,6 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index c931e1e0ea5..0661087a1ba 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -17,6 +17,14 @@
type: String,
required: true,
},
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
mixins: [
pipelinesMixin,
@@ -87,10 +95,12 @@
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
+ :empty-state-svg-path="emptyStateSvgPath"
/>
<error-state
v-if="shouldRenderErrorState"
+ :error-state-svg-path="errorStateSvgPath"
/>
<div
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 047544b1762..ae6b8902032 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,17 +1,19 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, wrap-iife, consistent-return,
+ no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
+ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
-window.CommitsList = (function() {
- var CommitsList = {};
+export default (function () {
+ const CommitsList = {};
CommitsList.timer = null;
- CommitsList.init = function(limit) {
+ CommitsList.init = function (limit) {
this.$contentList = $('.content_list');
- $("body").on("click", ".day-commits-table li.commit", function(e) {
- if (e.target.nodeName !== "A") {
- location.href = $(this).attr("url");
+ $('body').on('click', '.day-commits-table li.commit', function (e) {
+ if (e.target.nodeName !== 'A') {
+ location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
@@ -19,48 +21,47 @@ window.CommitsList = (function() {
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
- this.content = $("#commits-list");
- this.searchField = $("#commits-search");
+ this.content = $('#commits-list');
+ this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val();
return this.initSearch();
};
- CommitsList.initSearch = function() {
+ CommitsList.initSearch = function () {
this.timer = null;
- return this.searchField.keyup((function(_this) {
- return function() {
+ return this.searchField.keyup((function (_this) {
+ return function () {
clearTimeout(_this.timer);
return _this.timer = setTimeout(_this.filterResults, 500);
};
})(this));
};
- CommitsList.filterResults = function() {
- var commitsUrl, form, search;
- form = $(".commits-search-form");
- search = CommitsList.searchField.val();
+ CommitsList.filterResults = function () {
+ const form = $('.commits-search-form');
+ const search = CommitsList.searchField.val();
if (search === CommitsList.lastSearch) return;
- commitsUrl = form.attr("action") + '?' + form.serialize();
+ const commitsUrl = form.attr('action') + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5);
return $.ajax({
- type: "GET",
- url: form.attr("action"),
+ type: 'GET',
+ url: form.attr('action'),
data: form.serialize(),
- complete: function() {
+ complete: function () {
return CommitsList.content.fadeTo('fast', 1.0);
},
- success: function(data) {
+ success: function (data) {
CommitsList.lastSearch = search;
CommitsList.content.html(data.html);
return history.replaceState({
- page: commitsUrl
+ page: commitsUrl,
// Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
- error: function() {
+ error: function () {
CommitsList.lastSearch = null;
},
- dataType: "json"
+ dataType: 'json',
});
};
@@ -81,7 +82,7 @@ window.CommitsList = (function() {
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header.
- processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`);
+ processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index b78089525cc..cb5a9a9f6b5 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -12,4 +12,5 @@ import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
import './polyfills/element';
+import './polyfills/event';
import './polyfills/nodelist';
diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js
index aea61b82d03..db51ade61ae 100644
--- a/app/assets/javascripts/commons/polyfills/custom_event.js
+++ b/app/assets/javascripts/commons/polyfills/custom_event.js
@@ -1,7 +1,12 @@
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent');
- const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
+ const evtParams = {
+ bubbles: false,
+ cancelable: false,
+ detail: undefined,
+ ...params,
+ };
evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
return evt;
};
diff --git a/app/assets/javascripts/commons/polyfills/event.js b/app/assets/javascripts/commons/polyfills/event.js
new file mode 100644
index 00000000000..ff5b9a1982f
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/event.js
@@ -0,0 +1,18 @@
+/**
+ * Polyfill for IE11 support.
+ * new Event() is not supported by IE11.
+ * Although `initEvent` is deprecated for modern browsers it is the one supported by IE
+ */
+if (typeof window.Event !== 'function') {
+ window.Event = function Event(event, params) {
+ const evt = document.createEvent('Event');
+ const evtParams = {
+ bubbles: false,
+ cancelable: false,
+ ...params,
+ };
+ evt.initEvent(event, evtParams.bubbles, evtParams.cancelable);
+ return evt;
+ };
+ window.Event.prototype = Event;
+}
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index b375b61202e..eae4a7eab55 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
+import { rstrip } from './lib/utils/common_utils';
window.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
@@ -12,7 +13,7 @@ window.ConfirmDangerModal = (function() {
submit.disable();
$('.js-confirm-danger-input').off('input');
$('.js-confirm-danger-input').on('input', function() {
- if (gl.utils.rstrip($(this).val()) === project_path) {
+ if (rstrip($(this).val()) === project_path) {
return submit.enable();
} else {
return submit.disable();
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index 13ba4a57293..93b0cbf4209 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -1,6 +1,6 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import _ from 'underscore';
-import './lib/utils/common_utils';
+import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
const gfmRules = {
@@ -295,10 +295,10 @@ class CopyAsGFM {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
- const documentFragment = window.gl.utils.getSelectedFragment();
+ const documentFragment = getSelectedFragment();
if (!documentFragment) return;
- const el = transformer(documentFragment.cloneNode(true));
+ const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return;
e.preventDefault();
@@ -338,55 +338,64 @@ class CopyAsGFM {
}
static transformGFMSelection(documentFragment) {
- const gfmEls = documentFragment.querySelectorAll('.md, .wiki');
- switch (gfmEls.length) {
+ const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
+ switch (gfmElements.length) {
case 0: {
return documentFragment;
}
case 1: {
- return gfmEls[0];
+ return gfmElements[0];
}
default: {
- const allGfmEl = document.createElement('div');
+ const allGfmElement = document.createElement('div');
- for (let i = 0; i < gfmEls.length; i += 1) {
- const lineEl = gfmEls[i];
- allGfmEl.appendChild(lineEl);
- allGfmEl.appendChild(document.createTextNode('\n\n'));
+ for (let i = 0; i < gfmElements.length; i += 1) {
+ const gfmElement = gfmElements[i];
+ allGfmElement.appendChild(gfmElement);
+ allGfmElement.appendChild(document.createTextNode('\n\n'));
}
- return allGfmEl;
+ return allGfmElement;
}
}
}
- static transformCodeSelection(documentFragment) {
- const lineEls = documentFragment.querySelectorAll('.line');
+ static transformCodeSelection(documentFragment, target) {
+ let lineSelector = '.line';
- let codeEl;
- if (lineEls.length > 1) {
- codeEl = document.createElement('pre');
- codeEl.className = 'code highlight';
+ if (target) {
+ const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
+ if (lineClass) {
+ lineSelector = `.line_content.${lineClass} ${lineSelector}`;
+ }
+ }
+
+ const lineElements = documentFragment.querySelectorAll(lineSelector);
+
+ let codeElement;
+ if (lineElements.length > 1) {
+ codeElement = document.createElement('pre');
+ codeElement.className = 'code highlight';
- const lang = lineEls[0].getAttribute('lang');
+ const lang = lineElements[0].getAttribute('lang');
if (lang) {
- codeEl.setAttribute('lang', lang);
+ codeElement.setAttribute('lang', lang);
}
} else {
- codeEl = document.createElement('code');
+ codeElement = document.createElement('code');
}
- if (lineEls.length > 0) {
- for (let i = 0; i < lineEls.length; i += 1) {
- const lineEl = lineEls[i];
- codeEl.appendChild(lineEl);
- codeEl.appendChild(document.createTextNode('\n'));
+ if (lineElements.length > 0) {
+ for (let i = 0; i < lineElements.length; i += 1) {
+ const lineElement = lineElements[i];
+ codeElement.appendChild(lineElement);
+ codeElement.appendChild(document.createTextNode('\n'));
}
} else {
- codeEl.appendChild(documentFragment);
+ codeElement.appendChild(documentFragment);
}
- return codeEl;
+ return codeElement;
}
static nodeToGFM(node, respectWhitespaceParam = false) {
@@ -412,7 +421,7 @@ class CopyAsGFM {
for (const selector in rules) {
const func = rules[selector];
- if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
+ if (!nodeMatchesSelector(node, selector)) continue;
let result;
if (func.length === 2) {
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 907b468e576..3bed0678350 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
-class CreateLabelDropdown {
- constructor ($el, namespacePath, projectPath) {
+export default class CreateLabelDropdown {
+ constructor($el, namespacePath, projectPath) {
this.$el = $el;
this.namespacePath = namespacePath;
this.projectPath = projectPath;
@@ -22,7 +22,7 @@ class CreateLabelDropdown {
this.addBinding();
}
- cleanBinding () {
+ cleanBinding() {
this.$colorSuggestions.off('click');
this.$newLabelField.off('keyup change');
this.$newColorField.off('keyup change');
@@ -31,7 +31,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.off('click');
}
- addBinding () {
+ addBinding() {
const self = this;
this.$colorSuggestions.on('click', function (e) {
@@ -44,7 +44,7 @@ class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this));
- this.$cancelButton.on('click', function(e) {
+ this.$cancelButton.on('click', function (e) {
e.preventDefault();
e.stopPropagation();
@@ -55,7 +55,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
}
- addColorValue (e, $this) {
+ addColorValue(e, $this) {
e.preventDefault();
e.stopPropagation();
@@ -66,7 +66,7 @@ class CreateLabelDropdown {
.addClass('is-active');
}
- enableLabelCreateButton () {
+ enableLabelCreateButton() {
if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
this.$newLabelError.hide();
this.$newLabelCreateButton.enable();
@@ -75,7 +75,7 @@ class CreateLabelDropdown {
}
}
- resetForm () {
+ resetForm() {
this.$newLabelField
.val('')
.trigger('change');
@@ -90,13 +90,13 @@ class CreateLabelDropdown {
.removeClass('is-active');
}
- saveLabel (e) {
+ saveLabel(e) {
e.preventDefault();
e.stopPropagation();
Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(),
- color: this.$newColorField.val()
+ color: this.$newColorField.val(),
}, (label) => {
this.$newLabelCreateButton.enable();
@@ -107,8 +107,8 @@ class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
- ).join("<br/>");
+ `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ ).join('<br/>');
}
this.$newLabelError
@@ -122,6 +122,3 @@ class CreateLabelDropdown {
});
}
}
-
-window.gl = window.gl || {};
-gl.CreateLabelDropdown = CreateLabelDropdown;
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
new file mode 100644
index 00000000000..732697c134e
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -0,0 +1,55 @@
+<script>
+ import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
+
+ export default {
+ props: {
+ documentationLink: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ iconCycleAnalyticsSplash() {
+ return iconCycleAnalyticsSplash;
+ },
+ },
+ methods: {
+ dismissOverviewDialog() {
+ this.$emit('dismiss-overview-dialog');
+ },
+ },
+ };
+</script>
+<template>
+ <div class="landing content-block">
+ <button
+ class="js-ca-dismiss-button dismiss-button"
+ type="button"
+ :aria-label="__('Dismiss Cycle Analytics introduction box')"
+ @click="dismissOverviewDialog">
+ <i
+ class="fa fa-times"
+ aria-hidden="true">
+ </i>
+ </button>
+ <div class="svg-container" v-html="iconCycleAnalyticsSplash">
+ </div>
+ <div class="inner-content">
+ <h4>
+ {{__('Introducing Cycle Analytics')}}
+ </h4>
+ <p>
+ {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
+ </p>
+ <p>
+ <a
+ :href="documentationLink"
+ target="_blank"
+ rel="nofollow"
+ class="btn">
+ {{__('Read more')}}
+ </a>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
deleted file mode 100644
index 8d3d34f836f..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
- props: {
- count: {
- type: Number,
- required: true,
- },
- },
- template: `
- <span v-if="count === 50" class="events-info pull-right">
- <i class="fa fa-warning has-tooltip"
- aria-hidden="true"
- :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
- data-placement="top"></i>
- {{ n__('Showing %d event', 'Showing %d events', 50) }}
- </span>
- `,
-};
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
new file mode 100644
index 00000000000..6e94ba929b2
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
@@ -0,0 +1,26 @@
+<script>
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ };
+</script>
+<template>
+ <span v-if="count === 50" class="events-info pull-right">
+ <i
+ class="fa fa-warning"
+ v-tooltip
+ aria-hidden="true"
+ :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
+ data-placement="top"></i>
+ {{ n__('Showing %d event', 'Showing %d events', 50) }}
+ </span>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
deleted file mode 100644
index 7c32a38fbe7..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageCodeComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- components: {
- userAvatarImage,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="mergeRequest in items" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
- <h5 class="item-title merge-merquest-title">
- <a :href="mergeRequest.url">
- {{ mergeRequest.title }}
- </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
- &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="mergeRequest.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue
new file mode 100644
index 00000000000..45930145b0a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue
@@ -0,0 +1,51 @@
+<script>
+ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ userAvatarImage,
+ limitWarning,
+ totalTime,
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ {{ s__('OpenedNDaysAgo|Opened') }}
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ {{ s__('ByAuthor|by') }}
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
new file mode 100644
index 00000000000..8c98bd249a1
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
@@ -0,0 +1,57 @@
+<script>
+ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ userAvatarImage,
+ limitWarning,
+ totalTime,
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li
+ v-for="(issue, i) in items"
+ :key="i"
+ class="stage-event-item">
+ <div class="item-details">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="issue.author.avatarUrl"/>
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ {{ s__('OpenedNDaysAgo|Opened') }}
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ {{ s__('ByAuthor|by') }}
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"/>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
+
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
deleted file mode 100644
index 5f4a0ac8590..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageIssueComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- components: {
- userAvatarImage,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="issue in items" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="issue.author.avatarUrl"/>
- <h5 class="item-title issue-title">
- <a class="issue-title" :href="issue.url">
- {{ issue.title }}
- </a>
- </h5>
- <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
- &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="issue.author.webUrl" class="issue-author-link">
- {{ issue.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="issue.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
deleted file mode 100644
index 11fee5410d9..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import iconCommit from '../svg/icon_commit.svg';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StagePlanComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- components: {
- userAvatarImage,
- },
- data() {
- return { iconCommit };
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="commit in items" class="stage-event-item">
- <div class="item-details item-conmmit-component">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="commit.author.avatarUrl"/>
- <h5 class="item-title commit-title">
- <a :href="commit.commitUrl">
- {{ commit.title }}
- </a>
- </h5>
- <span>
- {{ s__('FirstPushedBy|First') }}
- <span class="commit-icon">${iconCommit}</span>
- <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
- {{ s__('FirstPushedBy|pushed by') }}
- <a :href="commit.author.webUrl" class="commit-author-link">
- {{ commit.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="commit.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue
new file mode 100644
index 00000000000..75d2f1fd70c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue
@@ -0,0 +1,60 @@
+<script>
+ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import iconCommit from '../svg/icon_commit.svg';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ userAvatarImage,
+ totalTime,
+ limitWarning,
+ },
+ computed: {
+ iconCommit() {
+ return iconCommit;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li
+ v-for="(commit, i) in items"
+ :key="i"
+ class="stage-event-item">
+ <div class="item-details item-conmmit-component">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="commit.author.avatarUrl"/>
+ <h5 class="item-title commit-title">
+ <a :href="commit.commitUrl">
+ {{ commit.title }}
+ </a>
+ </h5>
+ <span>
+ {{ s__('FirstPushedBy|First') }}
+ <span class="commit-icon" v-html="iconCommit"></span>
+ <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
+ {{ s__('FirstPushedBy|pushed by') }}
+ <a :href="commit.author.webUrl" class="commit-author-link">
+ {{ commit.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="commit.totalTime" />
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
+
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
deleted file mode 100644
index b7ba9360f70..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageProductionComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- components: {
- userAvatarImage,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="issue in items" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="issue.author.avatarUrl"/>
- <h5 class="item-title issue-title">
- <a class="issue-title" :href="issue.url">
- {{ issue.title }}
- </a>
- </h5>
- <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
- &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="issue.author.webUrl" class="issue-author-link">
- {{ issue.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="issue.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
deleted file mode 100644
index f41a0d0e4ff..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageReviewComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- components: {
- userAvatarImage,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="mergeRequest in items" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
- <h5 class="item-title merge-merquest-title">
- <a :href="mergeRequest.url">
- {{ mergeRequest.title }}
- </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
- &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
- </span>
- <template v-if="mergeRequest.state === 'closed'">
- <span class="merge-request-state">
- <i class="fa fa-ban"></i>
- {{ mergeRequest.state.toUpperCase() }}
- </span>
- </template>
- <template v-else>
- <span class="merge-request-branch" v-if="mergeRequest.branch">
- <i class= "fa fa-code-fork"></i>
- <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
- </span>
- </template>
- </div>
- <div class="item-time">
- <total-time :time="mergeRequest.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
new file mode 100644
index 00000000000..f54ea7df522
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -0,0 +1,66 @@
+<script>
+ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ userAvatarImage,
+ totalTime,
+ limitWarning,
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li
+ v-for="(mergeRequest, i) in items"
+ :key="i"
+ class="stage-event-item">
+ <div class="item-details">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ {{ s__('OpenedNDaysAgo|Opened') }}
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ {{ s__('ByAuthor|by') }}
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ <template v-if="mergeRequest.state === 'closed'">
+ <span class="merge-request-state">
+ <i class="fa fa-ban"></i>
+ {{ mergeRequest.state.toUpperCase() }}
+ </span>
+ </template>
+ <template v-else>
+ <span class="merge-request-branch" v-if="mergeRequest.branch">
+ <i class= "fa fa-code-fork"></i>
+ <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
+ </span>
+ </template>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"/>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
deleted file mode 100644
index d7c906c9d39..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import iconBranch from '../svg/icon_branch.svg';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageStagingComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- data() {
- return { iconBranch };
- },
- components: {
- userAvatarImage,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="build.author.avatarUrl"/>
- <h5 class="item-title">
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
- <span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="build-date">{{ build.date }}</a>
- {{ s__('ByAuthor|by') }}
- <a :href="build.author.webUrl" class="issue-author-link">
- {{ build.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
new file mode 100644
index 00000000000..5d95ddcd90e
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -0,0 +1,59 @@
+<script>
+ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import iconBranch from '../svg/icon_branch.svg';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ userAvatarImage,
+ totalTime,
+ limitWarning,
+ },
+ computed: {
+ iconBranch() {
+ return iconBranch;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li
+ v-for="(build, i) in items"
+ class="stage-event-item item-build-component"
+ :key="i">
+ <div class="item-details">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="build.author.avatarUrl"/>
+ <h5 class="item-title">
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
+ <span class="icon-branch" v-html="iconBranch"></span>
+ <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="build-date">{{ build.date }}</a>
+ {{ s__('ByAuthor|by') }}
+ <a :href="build.author.webUrl" class="issue-author-link">
+ {{ build.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"/>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
deleted file mode 100644
index 78cc97eea0b..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* eslint-disable no-param-reassign */
-import Vue from 'vue';
-import iconBuildStatus from '../svg/icon_build_status.svg';
-import iconBranch from '../svg/icon_branch.svg';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.StageTestComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- data() {
- return { iconBuildStatus, iconBranch };
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <h5 class="item-title">
- <span class="icon-build-status">${iconBuildStatus}</span>
- <a :href="build.url" class="item-build-name">{{ build.name }}</a>
- &middot;
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
- <span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="issue-date">
- {{ build.date }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
new file mode 100644
index 00000000000..04d5440b77b
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -0,0 +1,60 @@
+<script>
+ import iconBuildStatus from '../svg/icon_build_status.svg';
+ import iconBranch from '../svg/icon_branch.svg';
+ import limitWarning from './limit_warning_component.vue';
+ import totalTime from './total_time_component.vue';
+
+ export default {
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ components: {
+ totalTime,
+ limitWarning,
+ },
+ computed: {
+ iconBuildStatus() {
+ return iconBuildStatus;
+ },
+ iconBranch() {
+ return iconBranch;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li
+ v-for="(build, i) in items"
+ :key="i"
+ class="stage-event-item item-build-component">
+ <div class="item-details">
+ <h5 class="item-title">
+ <span class="icon-build-status" v-html="iconBuildStatus"></span>
+ <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+ &middot;
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
+ <span class="icon-branch" v-html="iconBranch"></span>
+ <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="issue-date">
+ {{ build.date }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"/>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
deleted file mode 100644
index d5e6167b2a8..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import Vue from 'vue';
-
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
-global.cycleAnalytics.TotalTimeComponent = Vue.extend({
- props: {
- time: Object,
- },
- template: `
- <span class="total-time">
- <template v-if="Object.keys(time).length">
- <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
- <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
- <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
- <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
- </template>
- <template v-else>
- --
- </template>
- </span>
- `,
-});
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
new file mode 100644
index 00000000000..62efd4f9c28
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
@@ -0,0 +1,29 @@
+<script>
+ export default {
+ props: {
+ time: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ hasData() {
+ return Object.keys(this.time).length;
+ },
+ },
+ };
+</script>
+<template>
+ <span class="total-time">
+ <template v-if="hasData">
+ <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
+ <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
+ <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
+ <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
+ </template>
+ <template v-else>
+ --
+ </template>
+ </span>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 6583e471a48..cdf5e3c0290 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -3,60 +3,63 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate';
-import LimitWarningComponent from './components/limit_warning_component';
-import './components/stage_code_component';
-import './components/stage_issue_component';
-import './components/stage_plan_component';
-import './components/stage_production_component';
-import './components/stage_review_component';
-import './components/stage_staging_component';
-import './components/stage_test_component';
-import './components/total_time_component';
-import './cycle_analytics_service';
-import './cycle_analytics_store';
+import banner from './components/banner.vue';
+import stageCodeComponent from './components/stage_code_component.vue';
+import stagePlanComponent from './components/stage_plan_component.vue';
+import stageComponent from './components/stage_component.vue';
+import stageReviewComponent from './components/stage_review_component.vue';
+import stageStagingComponent from './components/stage_staging_component.vue';
+import stageTestComponent from './components/stage_test_component.vue';
+import CycleAnalyticsService from './cycle_analytics_service';
+import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
- const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
- const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
- const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
- requestPath: cycleAnalyticsEl.dataset.requestPath,
- });
gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
- data: {
- state: cycleAnalyticsStore.state,
- isLoading: false,
- isLoadingStage: false,
- isEmptyStage: false,
- hasError: false,
- startDate: 30,
- isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
+ data() {
+ const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
+ const cycleAnalyticsService = new CycleAnalyticsService({
+ requestPath: cycleAnalyticsEl.dataset.requestPath,
+ });
+
+ return {
+ store: CycleAnalyticsStore,
+ state: CycleAnalyticsStore.state,
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+ hasError: false,
+ startDate: 30,
+ isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
+ service: cycleAnalyticsService,
+ };
},
computed: {
currentStage() {
- return cycleAnalyticsStore.currentActiveStage();
+ return this.store.currentActiveStage();
},
},
components: {
- 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
- 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
- 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
- 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
- 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
- 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
- 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
+ banner,
+ 'stage-issue-component': stageComponent,
+ 'stage-plan-component': stagePlanComponent,
+ 'stage-code-component': stageCodeComponent,
+ 'stage-test-component': stageTestComponent,
+ 'stage-review-component': stageReviewComponent,
+ 'stage-staging-component': stageStagingComponent,
+ 'stage-production-component': stageComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
methods: {
handleError() {
- cycleAnalyticsStore.setErrorState(true);
+ this.store.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.');
},
initDropdown() {
@@ -77,17 +80,17 @@ $(() => {
this.isLoading = true;
- cycleAnalyticsService
+ this.service
.fetchCycleAnalyticsData(fetchOptions)
- .done((response) => {
- cycleAnalyticsStore.setCycleAnalyticsData(response);
+ .then(resp => resp.json())
+ .then((response) => {
+ this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
+ this.isLoading = false;
})
- .error(() => {
+ .catch(() => {
this.handleError();
- })
- .always(() => {
this.isLoading = false;
});
},
@@ -100,27 +103,27 @@ $(() => {
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
- cycleAnalyticsStore.setActiveStage(stage);
+ this.store.setActiveStage(stage);
return;
}
this.isLoadingStage = true;
- cycleAnalyticsStore.setStageEvents([], stage);
- cycleAnalyticsStore.setActiveStage(stage);
+ this.store.setStageEvents([], stage);
+ this.store.setActiveStage(stage);
- cycleAnalyticsService
+ this.service
.fetchStageData({
stage,
startDate: this.startDate,
})
- .done((response) => {
+ .then(resp => resp.json())
+ .then((response) => {
this.isEmptyStage = !response.events.length;
- cycleAnalyticsStore.setStageEvents(response.events, stage);
+ this.store.setStageEvents(response.events, stage);
+ this.isLoadingStage = false;
})
- .error(() => {
+ .catch(() => {
this.isEmptyStage = true;
- })
- .always(() => {
this.isLoadingStage = false;
});
},
@@ -130,8 +133,4 @@ $(() => {
},
},
});
-
- // Register global components
- Vue.component('limit-warning', LimitWarningComponent);
- Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index 6504d7db2f2..f496c38208d 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,27 +1,16 @@
-/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
+Vue.use(VueResource);
-class CycleAnalyticsService {
+export default class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
+ this.cycleAnalytics = Vue.resource(this.requestPath);
}
- fetchCycleAnalyticsData(options) {
- options = options || { startDate: 30 };
-
- return $.ajax({
- url: this.requestPath,
- method: 'GET',
- dataType: 'json',
- contentType: 'application/json',
- data: {
- cycle_analytics: {
- start_date: options.startDate,
- },
- },
- });
+ fetchCycleAnalyticsData(options = { startDate: 30 }) {
+ return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
}
fetchStageData(options) {
@@ -30,12 +19,12 @@ class CycleAnalyticsService {
startDate,
} = options;
- return $.get(`${this.requestPath}/events/${stage.name}.json`, {
- cycle_analytics: {
- start_date: startDate,
+ return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
+ params: {
+ cycle_analytics: {
+ start_date: startDate,
+ },
},
});
}
}
-
-global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 991f8c1f6fd..8bf9ae17de0 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -4,9 +4,6 @@ import { __ } from '../locale';
import '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
-const global = window.gl || (window.gl = {});
-global.cycleAnalytics = global.cycleAnalytics || {};
-
const EMPTY_STAGE_TEXTS = {
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = {
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
};
-global.cycleAnalytics.CycleAnalyticsStore = {
+export default {
state: {
summary: '',
stats: '',
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 904f7f64fa8..b41d464475f 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -73,7 +73,7 @@
</span>
<a
v-if="deployKey.can_edit"
- class="btn btn-small"
+ class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 6a008112203..6c78662baa7 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -3,6 +3,7 @@
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
+import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
@@ -17,14 +18,18 @@ class Diff {
}
});
- FilesCommentButton.init($diffFile);
+ const tab = document.getElementById('diffs');
+ if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
- $diffFile.each((index, file) => new gl.ImageFile(file));
+ const firstFile = $('.files').first().get(0);
+ const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
+ $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) {
$(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
+ .on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
isBound = true;
}
@@ -100,6 +105,18 @@ class Diff {
this.highlightSelectedLine();
}
+ handleParallelLineDown(e) {
+ const line = $(e.currentTarget);
+ const table = line.closest('table');
+
+ table.removeClass('left-side-selected right-side-selected');
+
+ const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0];
+ if (lineClass) {
+ table.addClass(`${lineClass}-selected`);
+ }
+ }
+
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
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 298f737a2bc..e77910a83d4 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
@@ -169,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them.
$target.closest(".content").show();
- $target = $target.closest("tr.notes_holder");
+ const $notesHolder = $target.closest("tr.notes_holder");
+
+ // Image diff discussions does not use notes_holder
+ // so we should keep original $target value in those cases
+ if ($notesHolder.length > 0) {
+ $target = $notesHolder;
+ }
+
$target.show();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 96e5a440357..fe7cf8f5fc1 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index f3b537c83e2..1edd460f380 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -7,14 +7,11 @@
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
-/* global Commit */
-/* global CommitsList */
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsDropdown */
/* global GroupAvatar */
/* global LineHighlighter */
-/* global ProjectFork */
/* global BuildArtifacts */
/* global GroupsSelect */
/* global Search */
@@ -37,6 +34,7 @@
/* global Sidebar */
/* global ShortcutsWiki */
+import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
@@ -77,6 +75,9 @@ import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
+import AbuseReports from './abuse_reports';
+import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
+import AjaxLoadingSpinner from './ajax_loading_spinner';
(function() {
var Dispatcher;
@@ -100,7 +101,7 @@ import initChangesDropdown from './init_changes_dropdown';
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
+ const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
@@ -238,7 +239,7 @@ import initChangesDropdown from './init_changes_dropdown';
new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML));
break;
case 'projects:branches:index':
- gl.AjaxLoadingSpinner.init();
+ AjaxLoadingSpinner.init();
new DeleteModal();
break;
case 'projects:issues:new':
@@ -316,7 +317,6 @@ import initChangesDropdown from './init_changes_dropdown';
new gl.Activities();
break;
case 'projects:commit:show':
- new Commit();
new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
@@ -351,7 +351,7 @@ import initChangesDropdown from './init_changes_dropdown';
if ($('.blob-viewer').length) new BlobViewer();
if ($('.project-show-activity').length) new gl.Activities();
$('#tree-slider').waitForImages(function() {
- gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
+ ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
break;
case 'projects:edit':
@@ -427,7 +427,7 @@ import initChangesDropdown from './init_changes_dropdown';
new NewCommitForm($('.js-create-dir-form'));
new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
- gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
+ ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
break;
case 'projects:find_file:show':
@@ -475,7 +475,9 @@ import initChangesDropdown from './init_changes_dropdown';
shortcut_handler = true;
break;
case 'projects:forks:new':
- new ProjectFork();
+ import(/* webpackChunkName: 'project_fork' */ './project_fork')
+ .then(fork => fork.default())
+ .catch(() => {});
break;
case 'projects:artifacts:browse':
new ShortcutsNavigation();
@@ -523,6 +525,11 @@ import initChangesDropdown from './init_changes_dropdown';
case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors();
break;
+ case 'projects:clusters:show':
+ import(/* webpackChunkName: "clusters" */ './clusters')
+ .then(cluster => new cluster.default()) // eslint-disable-line new-cap
+ .catch(() => {});
+ break;
}
switch (path[0]) {
case 'sessions':
@@ -555,7 +562,7 @@ import initChangesDropdown from './init_changes_dropdown';
new Labels();
}
case 'abuse_reports':
- new gl.AbuseReports();
+ new AbuseReports();
break;
}
break;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 975903159be..1cba65d17cd 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -2,6 +2,7 @@
/* global Dropzone */
import _ from 'underscore';
import './preview_markdown';
+import csrf from './lib/utils/csrf';
window.DropzoneInput = (function() {
function DropzoneInput(form) {
@@ -50,9 +51,7 @@ window.DropzoneInput = (function() {
paramName: 'file',
maxFilesize: maxFileSize,
uploadMultiple: false,
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
- },
+ headers: csrf.headers,
previewContainer: false,
processing: function() {
return $('.div-dropzone-alert').alert('close');
@@ -260,9 +259,7 @@ window.DropzoneInput = (function() {
dataType: 'json',
processData: false,
contentType: false,
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
- },
+ headers: csrf.headers,
beforeSend: function() {
showSpinner();
return closeAlertMessage();
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index 91ed8c8467f..ce5f6219a3e 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -6,7 +6,7 @@ import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import '../../lib/utils/common_utils';
+import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
import eventHub from '../event_hub';
import Poll from '../../lib/utils/poll';
import environmentsMixin from '../mixins/environments_mixin';
@@ -51,19 +51,19 @@ export default {
computed: {
scope() {
- return gl.utils.getParameterByName('scope');
+ return getParameterByName('scope');
},
canReadEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
+ return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
+ return convertPermissionToBoolean(this.canCreateDeployment);
},
canCreateEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
+ return convertPermissionToBoolean(this.canCreateEnvironment);
},
},
@@ -72,8 +72,8 @@ export default {
* Toggles loading property.
*/
created() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const page = gl.utils.getParameterByName('page') || this.pageNumber;
+ const scope = getParameterByName('scope') || this.visibility;
+ const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
@@ -111,11 +111,11 @@ export default {
},
methods: {
- toggleFolder(folder, folderUrl) {
+ toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, folderUrl, true);
+ this.fetchChildEnvironments(folder, true);
}
},
@@ -126,15 +126,15 @@ export default {
* @return {String}
*/
changePage(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
+ const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const page = gl.utils.getParameterByName('page') || this.pageNumber;
+ const scope = getParameterByName('scope') || this.visibility;
+ const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
@@ -143,10 +143,10 @@ export default {
.catch(this.errorCallback);
},
- fetchChildEnvironments(folder, folderUrl, showLoader = false) {
+ fetchChildEnvironments(folder, showLoader = false) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
- this.service.getFolderContent(folderUrl)
+ this.service.getFolderContent(folder.folder_path)
.then(resp => resp.json())
.then(response => this.store.setfolderContent(folder, response.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
@@ -163,7 +163,7 @@ export default {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occured while making the request.'));
+ .catch(() => new Flash('An error occurred while making the request.'));
}
},
@@ -173,12 +173,7 @@ export default {
// We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders();
if (openFolders.length) {
- openFolders.forEach((folder) => {
- // TODO - Move this to the backend
- const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
-
- return this.fetchChildEnvironments(folder, folderUrl);
- });
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
}
},
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index d8b1b2f1b92..6de01fa53d0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -410,20 +410,11 @@ export default {
this.hasStopAction ||
this.canRetry;
},
-
- /**
- * Constructs folder URL based on the current location and the folder id.
- *
- * @return {String}
- */
- folderUrl() {
- return `${window.location.pathname}/folders/${this.model.folderName}`;
- },
},
methods: {
onClickFolder() {
- eventHub.$emit('toggleFolder', this.model, this.folderUrl);
+ eventHub.$emit('toggleFolder', this.model);
},
},
};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 925503a01c4..01e70c0bbb7 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -9,7 +9,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
-import '../../lib/utils/common_utils';
+import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
export default {
components: {
@@ -47,15 +47,15 @@ export default {
computed: {
scope() {
- return gl.utils.getParameterByName('scope');
+ return getParameterByName('scope');
},
canReadEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
+ return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
+ return convertPermissionToBoolean(this.canCreateDeployment);
},
/**
@@ -82,8 +82,8 @@ export default {
* Toggles loading property.
*/
created() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const page = gl.utils.getParameterByName('page') || this.pageNumber;
+ const scope = getParameterByName('scope') || this.visibility;
+ const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
@@ -125,15 +125,15 @@ export default {
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
+ const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const page = gl.utils.getParameterByName('page') || this.pageNumber;
+ const scope = getParameterByName('scope') || this.visibility;
+ const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
@@ -158,7 +158,7 @@ export default {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occured while making the request.'));
+ .catch(() => new Flash('An error occurred while making the request.'));
}
},
},
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 038c149be2d..aff8227c38c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,4 +1,4 @@
-import '~/lib/utils/common_utils';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
/**
* Environments Store.
*
@@ -66,8 +66,8 @@ export default class EnvironmentsStore {
}
setPagination(pagination = {}) {
- const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
- const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
+ const normalizedHeaders = normalizeHeaders(pagination);
+ const paginationInformation = parseIntPagination(normalizedHeaders);
this.state.paginationInformation = paginationInformation;
return paginationInformation;
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
deleted file mode 100644
index 800ca05cd11..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Cookies from 'js-cookie';
-import _ from 'underscore';
-import {
- getCookieName,
- getSelector,
- hidePopover,
- setupDismissButton,
- mouseenter,
- mouseleave,
-} from './feature_highlight_helper';
-
-export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => {
- const $selector = $(getSelector(id));
- const $parent = $selector.parent();
- const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
- const hideOnScroll = hidePopover.bind($selector);
- const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
-
- $selector
- // Setup popover
- .data('content', $popoverContent.prop('outerHTML'))
- .popover({
- html: true,
- // Override the existing template to add custom CSS classes
- template: `
- <div class="popover feature-highlight-popover" role="tooltip">
- <div class="arrow"></div>
- <div class="popover-content"></div>
- </div>
- `,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave)
- .on('inserted.bs.popover', setupDismissButton)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll);
- })
- .on('hide.bs.popover', () => {
- window.removeEventListener('scroll', hideOnScroll);
- })
- // Display feature highlight
- .removeAttr('disabled');
-};
-
-export const shouldHighlightFeature = (id) => {
- const element = document.querySelector(getSelector(id));
- const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true';
-
- return element && !previouslyDismissed;
-};
-
-export const highlightFeatures = (highlightOrder) => {
- const featureId = highlightOrder.find(shouldHighlightFeature);
-
- if (featureId) {
- setupFeatureHighlightPopover(featureId);
- return true;
- }
-
- return false;
-};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
deleted file mode 100644
index 9f741355cd7..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import Cookies from 'js-cookie';
-
-export const getCookieName = cookieId => `feature-highlighted-${cookieId}`;
-export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
-
-export const showPopover = function showPopover() {
- if (this.hasClass('js-popover-show')) {
- return false;
- }
- this.popover('show');
- this.addClass('disable-animation js-popover-show');
-
- return true;
-};
-
-export const hidePopover = function hidePopover() {
- if (!this.hasClass('js-popover-show')) {
- return false;
- }
- this.popover('hide');
- this.removeClass('disable-animation js-popover-show');
-
- return true;
-};
-
-export const dismiss = function dismiss(cookieId) {
- Cookies.set(getCookieName(cookieId), true);
- hidePopover.call(this);
- this.hide();
-};
-
-export const mouseleave = function mouseleave() {
- if (!$('.popover:hover').length > 0) {
- const $featureHighlight = $(this);
- hidePopover.call($featureHighlight);
- }
-};
-
-export const mouseenter = function mouseenter() {
- const $featureHighlight = $(this);
-
- const showedPopover = showPopover.call($featureHighlight);
- if (showedPopover) {
- $('.popover')
- .on('mouseleave', mouseleave.bind($featureHighlight));
- }
-};
-
-export const setupDismissButton = function setupDismissButton() {
- const popoverId = this.getAttribute('aria-describedby');
- const cookieId = this.dataset.highlight;
- const $popover = $(this);
- const dismissWrapper = dismiss.bind($popover, cookieId);
-
- $(`#${popoverId} .dismiss-feature-highlight`)
- .on('click', dismissWrapper);
-};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
deleted file mode 100644
index fd48f2e87cc..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_options.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { highlightFeatures } from './feature_highlight';
-import bp from '../breakpoints';
-
-const highlightOrder = ['issue-boards'];
-
-export default function domContentLoaded(order) {
- if (bp.getBreakpointSize() === 'lg') {
- highlightFeatures(order);
- }
-}
-
-document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder));
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index d02e4cd5876..a00d29a845a 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -7,6 +7,8 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
+import Cookies from 'js-cookie';
+
const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
- if (typeof notes !== 'undefined' && !this.isParallelView) {
- this.isParallelView = notes.isParallelView && notes.isParallelView();
- }
+ this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index f9bbbf0cbc1..ada14d2053c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
+ new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 0bc4b6f22a9..b32d589481d 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
preprocessing,
onError() {
/* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
+ new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 7246ccbb281..ce8817b1b2e 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
params: {
per_page: 20,
active: true,
+ group_id: this.getGroupId(),
project_id: this.getProjectId(),
current_user: true,
},
@@ -25,7 +26,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
},
onError() {
/* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
+ new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */
},
},
@@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown {
super.renderContent(forceShowList);
}
+ getGroupId() {
+ return this.input.getAttribute('data-group-id');
+ }
+
getProjectId() {
return this.input.getAttribute('data-project-id');
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 038239bf466..a44dc279a6f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -36,7 +36,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new window.Flash('An error occured while parsing recent searches');
+ new window.Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
@@ -332,7 +332,14 @@ class FilteredSearchManager {
const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => {
- if (t.classList.contains('js-visual-token')) {
+ let canClearToken = t.classList.contains('js-visual-token');
+
+ if (canClearToken) {
+ const tokenKey = t.querySelector('.name').textContent.trim();
+ canClearToken = this.canEdit && this.canEdit(tokenKey);
+ }
+
+ if (canClearToken) {
removeElements.push(t);
}
});
@@ -411,8 +418,14 @@ class FilteredSearchManager {
});
}
+ // allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
+ getAllParams(urlParams) {
+ return this.modifyUrlParams ? this.modifyUrlParams(urlParams) : urlParams;
+ }
+
loadSearchParamsFromURL() {
- const params = gl.utils.getUrlParamsArray();
+ const urlParams = gl.utils.getUrlParamsArray();
+ const params = this.getAllParams(urlParams);
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 4b19f7b4188..98837c3b2a0 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => {
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
- if (!currentOpenMenu) return 0;
+ if (!currentOpenMenu || !mousePos.length) return 0;
const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0];
@@ -77,10 +77,11 @@ export const hideMenu = (el) => {
export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight);
+ const left = sidebar ? sidebar.offsetWidth : 50;
const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
+ subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect();
@@ -148,7 +149,7 @@ export const documentMouseMove = (e) => {
export const subItemsMouseLeave = (relatedTarget) => {
clearTimeout(timeoutId);
- if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
+ if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
hideMenu(currentOpenMenu);
}
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 50d822eba5a..e8d8fef8579 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -548,6 +548,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.positionMenuAbove = function() {
var $menu = this.dropdown.find('.dropdown-menu');
+ $menu.addClass('dropdown-open-top');
$menu.css('top', 'initial');
$menu.css('bottom', '100%');
};
@@ -737,7 +738,7 @@ GitLabDropdown = (function() {
: selectedObject.id;
if (isInput) {
field = $(this.el);
- } else if (value) {
+ } else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
@@ -745,7 +746,7 @@ GitLabDropdown = (function() {
return;
}
- if (el.hasClass(ACTIVE_CLASS)) {
+ if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
@@ -851,7 +852,7 @@ GitLabDropdown = (function() {
if (href && href !== '#') {
gl.utils.visitUrl(href);
} else {
- $el.first().trigger('click');
+ $el.trigger('click');
}
}
};
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 36a04d4202f..d17a43b048a 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,6 +1,7 @@
<script>
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
+import { getParameterByName } from '../../lib/utils/common_utils';
export default {
props: {
@@ -18,8 +19,8 @@ export default {
},
methods: {
change(page) {
- const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
- const sortParam = gl.utils.getParameterByName('sort');
+ const filterGroupsParam = getParameterByName('filter_groups');
+ const sortParam = getParameterByName('sort');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
},
},
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index 439a931ddad..83b102764ba 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -1,5 +1,6 @@
import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
+import { getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
@@ -54,7 +55,7 @@ export default class GroupFilterableList extends FilterableList {
e.preventDefault();
const queryData = {};
- const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
+ const sortParam = getParameterByName('sort', e.currentTarget.href);
if (sortParam) {
queryData.sort = sortParam;
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 00e1bd94c9c..9ad8e5c6052 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -8,6 +8,7 @@ import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
+import { getParameterByName } from '../lib/utils/common_utils';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
@@ -58,17 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
this.isLoading = true;
}
- pageParam = gl.utils.getParameterByName('page');
+ pageParam = getParameterByName('page');
if (pageParam) {
page = pageParam;
}
- filterGroupsParam = gl.utils.getParameterByName('filter_groups');
+ filterGroupsParam = getParameterByName('filter_groups');
if (filterGroupsParam) {
filterGroups = filterGroupsParam;
}
- sortParam = gl.utils.getParameterByName('sort');
+ sortParam = getParameterByName('sort');
if (sortParam) {
sort = sortParam;
}
diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js
index 6eab6083e8f..f59ec677603 100644
--- a/app/assets/javascripts/groups/stores/groups_store.js
+++ b/app/assets/javascripts/groups/stores/groups_store.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default class GroupsStore {
constructor() {
@@ -30,8 +31,8 @@ export default class GroupsStore {
let paginationInfo;
if (Object.keys(pagination).length) {
- const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
- paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ const normalizedHeaders = normalizeHeaders(pagination);
+ paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 4d629bc6326..90ca70289ab 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,6 +4,7 @@
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
import Api from './api';
+import { normalizeCRLFHeaders } from './lib/utils/common_utils';
var slice = [].slice;
@@ -30,7 +31,7 @@ window.GroupsSelect = (function() {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
- const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
+ const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
diff --git a/app/assets/javascripts/help/help.js b/app/assets/javascripts/help/help.js
new file mode 100644
index 00000000000..4a22ebf187d
--- /dev/null
+++ b/app/assets/javascripts/help/help.js
@@ -0,0 +1,6 @@
+// We will render the icons list here
+if ($('#user-content-gitlab-icons').length > 0) {
+ const $iconsHeader = $('#user-content-gitlab-icons');
+ const $iconsList = $('<div id="iconsList">ICONS</div>');
+ $($iconsList).insertAfter($iconsHeader.parent());
+}
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
new file mode 100644
index 00000000000..6a6a668308d
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -0,0 +1,38 @@
+export function createImageBadge(noteId, { x, y }, classNames = []) {
+ const buttonEl = document.createElement('button');
+ const classList = classNames.concat(['js-image-badge']);
+ classList.forEach(className => buttonEl.classList.add(className));
+ buttonEl.setAttribute('type', 'button');
+ buttonEl.setAttribute('disabled', true);
+ buttonEl.dataset.noteId = noteId;
+ buttonEl.style.left = `${x}px`;
+ buttonEl.style.top = `${y}px`;
+
+ return buttonEl;
+}
+
+export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
+ const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
+ buttonEl.innerText = badgeText;
+
+ containerEl.appendChild(buttonEl);
+}
+
+export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
+ const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
+ const iconEl = document.createElement('i');
+ iconEl.className = 'fa fa-comment-o';
+ iconEl.setAttribute('aria-label', 'comment');
+
+ buttonEl.appendChild(iconEl);
+ containerEl.appendChild(buttonEl);
+}
+
+export function addAvatarBadge(el, event) {
+ const { noteId, badgeNumber } = event.detail;
+
+ // Add badge to new comment
+ const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
+ avatarBadgeEl.innerText = badgeNumber;
+ avatarBadgeEl.classList.remove('hidden');
+}
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
new file mode 100644
index 00000000000..05000c73052
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -0,0 +1,58 @@
+export function addCommentIndicator(containerEl, { x, y }) {
+ const buttonEl = document.createElement('button');
+ buttonEl.classList.add('btn-transparent');
+ buttonEl.classList.add('comment-indicator');
+ buttonEl.setAttribute('type', 'button');
+ buttonEl.style.left = `${x}px`;
+ buttonEl.style.top = `${y}px`;
+
+ buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
+
+ containerEl.appendChild(buttonEl);
+}
+
+export function removeCommentIndicator(imageFrameEl) {
+ const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
+ const imageEl = imageFrameEl.querySelector('img');
+ const willRemove = !!commentIndicatorEl;
+ let meta = {};
+
+ if (willRemove) {
+ meta = {
+ x: parseInt(commentIndicatorEl.style.left, 10),
+ y: parseInt(commentIndicatorEl.style.top, 10),
+ image: {
+ width: imageEl.width,
+ height: imageEl.height,
+ },
+ };
+
+ commentIndicatorEl.remove();
+ }
+
+ return Object.assign({}, meta, {
+ removed: willRemove,
+ });
+}
+
+export function showCommentIndicator(imageFrameEl, coordinate) {
+ const { x, y } = coordinate;
+ const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
+
+ if (commentIndicatorEl) {
+ commentIndicatorEl.style.left = `${x}px`;
+ commentIndicatorEl.style.top = `${y}px`;
+ } else {
+ addCommentIndicator(imageFrameEl, coordinate);
+ }
+}
+
+export function commentIndicatorOnClick(event) {
+ // Prevent from triggering onAddImageDiffNote in notes.js
+ event.stopPropagation();
+
+ const buttonEl = event.currentTarget;
+ const diffViewerEl = buttonEl.closest('.diff-viewer');
+ const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea');
+ textareaEl.focus();
+}
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
new file mode 100644
index 00000000000..12d56714b34
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -0,0 +1,44 @@
+export function setPositionDataAttribute(el, options) {
+ // Update position data attribute so that the
+ // new comment form can use this data for ajax request
+ const { x, y, width, height } = options;
+ const position = el.dataset.position;
+ const positionObject = Object.assign({}, JSON.parse(position), {
+ x,
+ y,
+ width,
+ height,
+ });
+
+ el.setAttribute('data-position', JSON.stringify(positionObject));
+}
+
+export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
+ const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
+ avatarBadgeEl.innerText = newBadgeNumber;
+}
+
+export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
+ const discussionBadgeEl = discussionEl.querySelector('.badge');
+ discussionBadgeEl.innerText = newBadgeNumber;
+}
+
+export function toggleCollapsed(event) {
+ const toggleButtonEl = event.currentTarget;
+ const discussionNotesEl = toggleButtonEl.closest('.discussion-notes');
+ const formEl = discussionNotesEl.querySelector('.discussion-form');
+ const isCollapsed = discussionNotesEl.classList.contains('collapsed');
+
+ if (isCollapsed) {
+ discussionNotesEl.classList.remove('collapsed');
+ } else {
+ discussionNotesEl.classList.add('collapsed');
+ }
+
+ // Override the inline display style set in notes.js
+ if (formEl && !isCollapsed) {
+ formEl.style.display = 'none';
+ } else if (formEl && isCollapsed) {
+ formEl.style.display = 'block';
+ }
+}
diff --git a/app/assets/javascripts/image_diff/helpers/index.js b/app/assets/javascripts/image_diff/helpers/index.js
new file mode 100644
index 00000000000..4a100631003
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/index.js
@@ -0,0 +1,25 @@
+import * as badgeHelper from './badge_helper';
+import * as commentIndicatorHelper from './comment_indicator_helper';
+import * as domHelper from './dom_helper';
+import * as utilsHelper from './utils_helper';
+
+export default {
+ addCommentIndicator: commentIndicatorHelper.addCommentIndicator,
+ removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator,
+ showCommentIndicator: commentIndicatorHelper.showCommentIndicator,
+ commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick,
+
+ addImageBadge: badgeHelper.addImageBadge,
+ addImageCommentBadge: badgeHelper.addImageCommentBadge,
+ addAvatarBadge: badgeHelper.addAvatarBadge,
+
+ setPositionDataAttribute: domHelper.setPositionDataAttribute,
+ updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber,
+ updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber,
+ toggleCollapsed: domHelper.toggleCollapsed,
+
+ resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement,
+ generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM,
+ getTargetSelection: utilsHelper.getTargetSelection,
+ initImageDiff: utilsHelper.initImageDiff,
+};
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
new file mode 100644
index 00000000000..96fc735e629
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -0,0 +1,95 @@
+import ImageBadge from '../image_badge';
+import ImageDiff from '../image_diff';
+import ReplacedImageDiff from '../replaced_image_diff';
+import '../../commit/image_file';
+
+export function resizeCoordinatesToImageElement(imageEl, meta) {
+ const { x, y, width, height } = meta;
+
+ const imageWidth = imageEl.width;
+ const imageHeight = imageEl.height;
+
+ const widthRatio = imageWidth / width;
+ const heightRatio = imageHeight / height;
+
+ return {
+ x: Math.round(x * widthRatio),
+ y: Math.round(y * heightRatio),
+ width: imageWidth,
+ height: imageHeight,
+ };
+}
+
+export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) {
+ const position = JSON.parse(discussionEl.dataset.position);
+ const firstNoteEl = discussionEl.querySelector('.note');
+ const badge = new ImageBadge({
+ actual: position,
+ imageEl: imageFrameEl.querySelector('img'),
+ noteId: firstNoteEl.id,
+ discussionId: discussionEl.dataset.discussionId,
+ });
+
+ return badge;
+}
+
+export function getTargetSelection(event) {
+ const containerEl = event.currentTarget;
+ const imageEl = containerEl.querySelector('img');
+
+ const x = event.offsetX;
+ const y = event.offsetY;
+
+ const width = imageEl.width;
+ const height = imageEl.height;
+
+ const actualWidth = imageEl.naturalWidth;
+ const actualHeight = imageEl.naturalHeight;
+
+ const widthRatio = actualWidth / width;
+ const heightRatio = actualHeight / height;
+
+ // Browser will include the frame as a clickable target,
+ // which would result in potential 1px out of bounds value
+ // This bound the coordinates to inside the frame
+ const normalizedX = Math.max(0, x) && Math.min(x, width);
+ const normalizedY = Math.max(0, y) && Math.min(y, height);
+
+ return {
+ browser: {
+ x: normalizedX,
+ y: normalizedY,
+ width,
+ height,
+ },
+ actual: {
+ // Round x, y so that we don't need to deal with decimals
+ x: Math.round(normalizedX * widthRatio),
+ y: Math.round(normalizedY * heightRatio),
+ width: actualWidth,
+ height: actualHeight,
+ },
+ };
+}
+
+export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
+ const options = {
+ canCreateNote,
+ renderCommentBadge,
+ };
+ let diff;
+
+ // ImageFile needs to be invoked before initImageDiff so that badges
+ // can mount to the correct location
+ new gl.ImageFile(fileEl); // eslint-disable-line no-new
+
+ if (fileEl.querySelector('.diff-file .js-single-image')) {
+ diff = new ImageDiff(fileEl, options);
+ diff.init();
+ } else if (fileEl.querySelector('.diff-file .js-replaced-image')) {
+ diff = new ReplacedImageDiff(fileEl, options);
+ diff.init();
+ }
+
+ return diff;
+}
diff --git a/app/assets/javascripts/image_diff/image_badge.js b/app/assets/javascripts/image_diff/image_badge.js
new file mode 100644
index 00000000000..51a8cda98d7
--- /dev/null
+++ b/app/assets/javascripts/image_diff/image_badge.js
@@ -0,0 +1,23 @@
+import imageDiffHelper from './helpers/index';
+
+const defaultMeta = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+};
+
+export default class ImageBadge {
+ constructor(options) {
+ const { noteId, discussionId } = options;
+
+ this.actual = options.actual || defaultMeta;
+ this.browser = options.browser || defaultMeta;
+ this.noteId = noteId;
+ this.discussionId = discussionId;
+
+ if (options.imageEl && !options.browser) {
+ this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual);
+ }
+ }
+}
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
new file mode 100644
index 00000000000..f3af92cf2b0
--- /dev/null
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -0,0 +1,143 @@
+import imageDiffHelper from './helpers/index';
+import ImageBadge from './image_badge';
+import { isImageLoaded } from '../lib/utils/image_utility';
+
+export default class ImageDiff {
+ constructor(el, options) {
+ this.el = el;
+ this.canCreateNote = !!(options && options.canCreateNote);
+ this.renderCommentBadge = !!(options && options.renderCommentBadge);
+ this.$noteContainer = $('.note-container', this.el);
+ this.imageBadges = [];
+ }
+
+ init() {
+ this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame');
+ this.imageEl = this.imageFrameEl.querySelector('img');
+
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.imageClickedWrapper = this.imageClicked.bind(this);
+ this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl);
+ this.addBadgeWrapper = this.addBadge.bind(this);
+ this.removeBadgeWrapper = this.removeBadge.bind(this);
+ this.renderBadgesWrapper = this.renderBadges.bind(this);
+
+ // Render badges
+ if (isImageLoaded(this.imageEl)) {
+ this.renderBadges();
+ } else {
+ this.imageEl.addEventListener('load', this.renderBadgesWrapper);
+ }
+
+ // jquery makes the event delegation here much simpler
+ this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed);
+ $(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick);
+
+ if (this.canCreateNote) {
+ this.el.addEventListener('click.imageDiff', this.imageClickedWrapper);
+ this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper);
+ this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper);
+ this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper);
+ }
+ }
+
+ imageClicked(event) {
+ const customEvent = event.detail;
+ const selection = imageDiffHelper.getTargetSelection(customEvent);
+ const el = customEvent.currentTarget;
+
+ imageDiffHelper.setPositionDataAttribute(el, selection.actual);
+ imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser);
+ }
+
+ renderBadges() {
+ const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes');
+ [...discussionsEls].forEach(this.renderBadge.bind(this));
+ }
+
+ renderBadge(discussionEl, index) {
+ const imageBadge = imageDiffHelper
+ .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
+
+ this.imageBadges.push(imageBadge);
+
+ const options = {
+ coordinate: imageBadge.browser,
+ noteId: imageBadge.noteId,
+ };
+
+ if (this.renderCommentBadge) {
+ imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
+ } else {
+ const numberBadgeOptions = Object.assign({}, options, {
+ badgeText: index + 1,
+ });
+
+ imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
+ }
+ }
+
+ addBadge(event) {
+ const { x, y, width, height, noteId, discussionId } = event.detail;
+ const badgeText = this.imageBadges.length + 1;
+ const imageBadge = new ImageBadge({
+ actual: {
+ x,
+ y,
+ width,
+ height,
+ },
+ imageEl: this.imageFrameEl.querySelector('img'),
+ noteId,
+ discussionId,
+ });
+
+ this.imageBadges.push(imageBadge);
+
+ imageDiffHelper.addImageBadge(this.imageFrameEl, {
+ coordinate: imageBadge.browser,
+ badgeText,
+ noteId,
+ });
+
+ imageDiffHelper.addAvatarBadge(this.el, {
+ detail: {
+ noteId,
+ badgeNumber: badgeText,
+ },
+ });
+
+ const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
+ imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText);
+ }
+
+ removeBadge(event) {
+ const { badgeNumber } = event.detail;
+ const indexToRemove = badgeNumber - 1;
+ const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
+
+ if (this.imageBadges.length !== badgeNumber) {
+ // Cascade badges count numbers for (avatar badges + image badges)
+ this.imageBadges.forEach((badge, index) => {
+ if (index > indexToRemove) {
+ const { discussionId } = badge;
+ const updatedBadgeNumber = index;
+ const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
+
+ imageBadgeEls[index].innerText = updatedBadgeNumber;
+
+ imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
+ imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
+ }
+ });
+ }
+
+ this.imageBadges.splice(indexToRemove, 1);
+
+ const imageBadgeEl = imageBadgeEls[indexToRemove];
+ imageBadgeEl.remove();
+ }
+}
diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js
new file mode 100644
index 00000000000..2f16c6ef115
--- /dev/null
+++ b/app/assets/javascripts/image_diff/init_discussion_tab.js
@@ -0,0 +1,12 @@
+import imageDiffHelper from './helpers/index';
+
+export default () => {
+ // Always pass can-create-note as false because a user
+ // cannot place new badge markers on discussion tab
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+
+ const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
+ [...diffFileEls].forEach(diffFileEl =>
+ imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
+};
diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js
new file mode 100644
index 00000000000..4abd13fb472
--- /dev/null
+++ b/app/assets/javascripts/image_diff/replaced_image_diff.js
@@ -0,0 +1,92 @@
+import imageDiffHelper from './helpers/index';
+import { viewTypes, isValidViewType } from './view_types';
+import ImageDiff from './image_diff';
+
+export default class ReplacedImageDiff extends ImageDiff {
+ init(defaultViewType = viewTypes.TWO_UP) {
+ this.imageFrameEls = {
+ [viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'),
+ [viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'),
+ [viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'),
+ };
+
+ const viewModesEl = this.el.querySelector('.view-modes-menu');
+ this.viewModesEls = {
+ [viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'),
+ [viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'),
+ [viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'),
+ };
+
+ this.currentView = defaultViewType;
+ this.generateImageEls();
+ this.bindEvents();
+ }
+
+ generateImageEls() {
+ this.imageEls = {};
+
+ const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
+ viewTypeNames.forEach((viewType) => {
+ this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
+ });
+ }
+
+ bindEvents() {
+ super.bindEvents();
+
+ this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP);
+ this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE);
+ this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN);
+
+ this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp);
+ this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe);
+ this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin);
+ }
+
+ get imageEl() {
+ return this.imageEls[this.currentView];
+ }
+
+ get imageFrameEl() {
+ return this.imageFrameEls[this.currentView];
+ }
+
+ changeView(newView) {
+ if (!isValidViewType(newView)) {
+ return;
+ }
+
+ const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl);
+
+ this.currentView = newView;
+
+ // Clear existing badges on new view
+ const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
+ [...existingBadges].map(badge => badge.remove());
+
+ // Remove existing references to old view image badges
+ this.imageBadges = [];
+
+ // Image_file.js has a fade animation of 200ms for loading the view
+ // Need to wait an additional 250ms for the images to be displayed
+ // on window in order to re-normalize their dimensions
+ setTimeout(this.renderNewView.bind(this, indicator), 250);
+ }
+
+ renderNewView(indicator) {
+ // Generate badge coordinates on new view
+ this.renderBadges();
+
+ // Re-render indicator in new view
+ if (indicator.removed) {
+ const normalizedIndicator = imageDiffHelper
+ .resizeCoordinatesToImageElement(this.imageEl, {
+ x: indicator.x,
+ y: indicator.y,
+ width: indicator.image.width,
+ height: indicator.image.height,
+ });
+ imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
+ }
+ }
+}
diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js
new file mode 100644
index 00000000000..ab0a595571f
--- /dev/null
+++ b/app/assets/javascripts/image_diff/view_types.js
@@ -0,0 +1,9 @@
+export const viewTypes = {
+ TWO_UP: 'TWO_UP',
+ SWIPE: 'SWIPE',
+ ONION_SKIN: 'ONION_SKIN',
+};
+
+export function isValidViewType(validate) {
+ return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
+}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 70c364e51fe..1d305f1eb2f 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -67,10 +67,13 @@ const PARTICIPANTS_ROW_COUNT = 7;
originalText = $(this).data("original-text");
if (currentText === originalText) {
$(this).text(lessText);
+
+ if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
}
- return $(".js-participants-hidden").toggle();
+
+ $(".js-participants-hidden").toggle();
};
return IssuableContext;
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e115ee40219..06f6ec241f4 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -72,10 +72,6 @@ export default {
required: false,
default: () => [],
},
- isConfidential: {
- type: Boolean,
- required: true,
- },
markdownPreviewPath: {
type: String,
required: true,
@@ -131,7 +127,6 @@ export default {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
- confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
@@ -147,8 +142,6 @@ export default {
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
- } else if (data.confidential !== this.isConfidential) {
- gl.utils.visitUrl(location.pathname);
}
return this.service.getData();
diff --git a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
deleted file mode 100644
index a0ff08e9111..00000000000
--- a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<script>
- export default {
- props: {
- formState: {
- type: Object,
- required: true,
- },
- },
- };
-</script>
-
-<template>
- <fieldset class="checkbox">
- <label for="issue-confidential">
- <input
- type="checkbox"
- value="1"
- id="issue-confidential"
- v-model="formState.confidential" />
- This issue is confidential and should only be visible to team members with at least Reporter access.
- </label>
- </fieldset>
-</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 6a2dd502fe2..28bf6c67ea5 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -4,7 +4,6 @@
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
- import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
@@ -44,7 +43,6 @@
descriptionField,
descriptionTemplate,
editActions,
- confidentialCheckbox,
},
computed: {
hasIssuableTemplates() {
@@ -81,8 +79,6 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" />
- <confidential-checkbox
- :form-state="formState" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 8053ef57e6c..aca9dec2a96 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
- isConfidential: this.isConfidential,
markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index f4639e9ed2a..af8b0414266 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -3,7 +3,6 @@ export default class Store {
this.state = initialState;
this.formState = {
title: '',
- confidential: false,
description: '',
lockedWarningVisible: false,
updateLoading: false,
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 7d7f91227f9..d479f7ed682 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,6 +4,7 @@
import _ from 'underscore';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
+import CreateLabelDropdown from './create_label';
(function() {
this.LabelsSelect = (function() {
@@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
@@ -127,13 +128,6 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$('.has-tooltip', $value).tooltip({
container: 'body'
});
- return $value.find('a').each(function(i) {
- return setTimeout((function(_this) {
- return function() {
- return gl.animate.animate($(_this), 'pulse');
- };
- })(this), 200 * i);
- });
});
};
$dropdown.glDropdown({
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
deleted file mode 100644
index d93c1d0da59..00000000000
--- a/app/assets/javascripts/lib/utils/animate.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
-(function() {
- (function(w) {
- if (w.gl == null) {
- w.gl = {};
- }
- if (gl.animate == null) {
- gl.animate = {};
- }
- gl.animate.animate = function($el, animation, options, done) {
- if ((options != null ? options.cssStart : void 0) != null) {
- $el.css(options.cssStart);
- }
- $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
- $(this).removeClass(animation + ' animated');
- if (done != null) {
- done();
- }
- if ((options != null ? options.cssEnd : void 0) != null) {
- $el.css(options.cssEnd);
- }
- });
- };
- gl.animate.animateEach = function($els, animation, time, options, done) {
- var dfd;
- dfd = $.Deferred();
- if (!$els.length) {
- dfd.resolve();
- }
- $els.each(function(i) {
- setTimeout((function(_this) {
- return function() {
- var $this;
- $this = $(_this);
- return gl.animate.animate($this, animation, options, function() {
- if (i === $els.length - 1) {
- dfd.resolve();
- if (done != null) {
- return done();
- }
- }
- });
- };
- })(this), time * i);
- });
- return dfd.promise();
- };
- })(window);
-}).call(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index b8bebe1894f..423a25fbdfa 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,437 +1,438 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
-(function() {
- (function(w) {
- var base;
- const faviconEl = document.getElementById('favicon');
- const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
- w.gl || (w.gl = {});
- (base = w.gl).utils || (base.utils = {});
- w.gl.utils.isInGroupsPage = function() {
- return gl.utils.getPagePath() === 'groups';
- };
- w.gl.utils.isInProjectPage = function() {
- return gl.utils.getPagePath() === 'projects';
- };
- w.gl.utils.getProjectSlug = function() {
- if (this.isInProjectPage()) {
- return $('body').data('project');
- } else {
- return null;
- }
- };
- w.gl.utils.getGroupSlug = function() {
- if (this.isInGroupsPage()) {
- return $('body').data('group');
- } else {
- return null;
- }
- };
-
- w.gl.utils.isInIssuePage = () => {
- const page = gl.utils.getPagePath(1);
- const action = gl.utils.getPagePath(2);
-
- return page === 'issues' && action === 'show';
- };
- w.gl.utils.ajaxGet = function(url) {
- return $.ajax({
- type: "GET",
- url: url,
- dataType: "script"
- });
- };
-
- w.gl.utils.ajaxPost = function(url, data) {
- return $.ajax({
- type: 'POST',
- url: url,
- data: data,
- });
- };
-
- w.gl.utils.extractLast = function(term) {
- return this.split(term).pop();
- };
-
- w.gl.utils.rstrip = function rstrip(val) {
- if (val) {
- return val.replace(/\s+$/, '');
+export const getPagePath = (index = 0) => $('body').data('page').split(':')[index];
+
+export const isInGroupsPage = () => getPagePath() === 'groups';
+
+export const isInProjectPage = () => getPagePath() === 'projects';
+
+export const getProjectSlug = () => {
+ if (isInProjectPage()) {
+ return $('body').data('project');
+ }
+ return null;
+};
+
+export const getGroupSlug = () => {
+ if (isInGroupsPage()) {
+ return $('body').data('group');
+ }
+ return null;
+};
+
+export const isInIssuePage = () => {
+ const page = getPagePath(1);
+ const action = getPagePath(2);
+
+ return page === 'issues' && action === 'show';
+};
+
+export const ajaxGet = url => $.ajax({
+ type: 'GET',
+ url,
+ dataType: 'script',
+});
+
+export const ajaxPost = (url, data) => $.ajax({
+ type: 'POST',
+ url,
+ data,
+});
+
+export const rstrip = (val) => {
+ if (val) {
+ return val.replace(/\s+$/, '');
+ }
+ return val;
+};
+
+export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+
+export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
+ const field = $(fieldSelector);
+ const closestSubmit = field.closest('form').find(buttonSelector);
+ if (rstrip(field.val()) === '') {
+ closestSubmit.disable();
+ }
+ // eslint-disable-next-line func-names
+ return field.on(eventName, function () {
+ if (rstrip($(this).val()) === '') {
+ return closestSubmit.disable();
+ }
+ return closestSubmit.enable();
+ });
+};
+
+// automatically adjust scroll position for hash urls taking the height of the navbar into account
+// https://github.com/twitter/bootstrap/issues/1768
+export const handleLocationHash = () => {
+ let hash = window.gl.utils.getLocationHash();
+ if (!hash) return;
+
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
+
+ const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
+ const fixedTabs = document.querySelector('.js-tabs-affix');
+ const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
+ const fixedNav = document.querySelector('.navbar-gitlab');
+
+ let adjustment = 0;
+ if (fixedNav) adjustment -= fixedNav.offsetHeight;
+
+ if (target && target.scrollIntoView) {
+ target.scrollIntoView(true);
+ }
+
+ if (fixedTabs) {
+ adjustment -= fixedTabs.offsetHeight;
+ }
+
+ if (fixedDiffStats) {
+ adjustment -= fixedDiffStats.offsetHeight;
+ }
+
+ window.scrollBy(0, adjustment);
+};
+
+// Check if element scrolled into viewport from above or below
+// Courtesy http://stackoverflow.com/a/7557433/414749
+export const isInViewport = (el) => {
+ const rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+};
+
+export const parseUrl = (url) => {
+ const parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+};
+
+export const parseUrlPathname = (url) => {
+ const parsedUrl = parseUrl(url);
+ // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
+ // We have to make sure we always have an absolute path.
+ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
+};
+
+// We can trust that each param has one & since values containing & will be encoded
+// Remove the first character of search as it is always ?
+export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+});
+
+export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
+
+// Identify following special clicks
+// 1) Cmd + Click on Mac (e.metaKey)
+// 2) Ctrl + Click on PC (e.ctrlKey)
+// 3) Middle-click or Mouse Wheel Click (e.which is 2)
+export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
+
+export const scrollToElement = ($el) => {
+ const top = $el.offset().top;
+ const mrTabsHeight = $('.merge-request-tabs').height() || 0;
+ const headerHeight = $('.navbar-gitlab').height() || 0;
+
+ return $('body, html').animate({
+ scrollTop: top - mrTabsHeight - headerHeight,
+ }, 200);
+};
+
+/**
+ this will take in the `name` of the param you want to parse in the url
+ if the name does not exist this function will return `null`
+ otherwise it will return the value of the param key provided
+*/
+export const getParameterByName = (name, urlToParse) => {
+ const url = urlToParse || window.location.href;
+ const parsedName = name.replace(/[[\]]/g, '\\$&');
+ const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
+ const results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+};
+
+export const getSelectedFragment = () => {
+ const selection = window.getSelection();
+ if (selection.rangeCount === 0) return null;
+ 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;
+};
+
+// TODO: Update this name, there is a gl.text.insertText function.
+export const insertText = (target, text) => {
+ // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
+ const selectionStart = target.selectionStart;
+ const selectionEnd = target.selectionEnd;
+ const value = target.value;
+
+ const textBefore = value.substring(0, selectionStart);
+ const textAfter = value.substring(selectionEnd, value.length);
+
+ const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
+ const newText = textBefore + insertedText + textAfter;
+
+ // eslint-disable-next-line no-param-reassign
+ target.value = newText;
+ // eslint-disable-next-line no-param-reassign
+ target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
+
+ // Trigger autosave
+ $(target).trigger('input');
+
+ // Trigger autosize
+ const event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ target.dispatchEvent(event);
+};
+
+export const nodeMatchesSelector = (node, selector) => {
+ const matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector;
+
+ if (matches) {
+ return matches.call(node, selector);
+ }
+
+ // IE11 doesn't support `node.matches(selector)`
+
+ let parentNode = node.parentNode;
+ if (!parentNode) {
+ parentNode = document.createElement('div');
+ // eslint-disable-next-line no-param-reassign
+ node = node.cloneNode(true);
+ parentNode.appendChild(node);
+ }
+
+ const matchingNodes = parentNode.querySelectorAll(selector);
+ return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
+};
+
+/**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+*/
+export const normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+};
+
+/**
+ this will take in the getAllResponseHeaders result and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+*/
+export const normalizeCRLFHeaders = (headers) => {
+ const headersObject = {};
+ const headersArray = headers.split('\n');
+
+ headersArray.forEach((header) => {
+ const keyValue = header.split(': ');
+ headersObject[keyValue[0]] = keyValue[1];
+ });
+
+ return normalizeHeaders(headersObject);
+};
+
+/**
+ * Parses pagination object string values into numbers.
+ *
+ * @param {Object} paginationInformation
+ * @returns {Object}
+ */
+export const parseIntPagination = paginationInformation => ({
+ perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
+ page: parseInt(paginationInformation['X-PAGE'], 10),
+ total: parseInt(paginationInformation['X-TOTAL'], 10),
+ totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
+ nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
+ previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
+});
+
+/**
+ * Updates the search parameter of a URL given the parameter and value provided.
+ *
+ * If no search params are present we'll add it.
+ * If param for page is already present, we'll update it
+ * If there are params but not for the given one, we'll add it at the end.
+ * Returns the new search parameters.
+ *
+ * @param {String} param
+ * @param {Number|String|Undefined|Null} value
+ * @return {String}
+ */
+export const setParamInURL = (param, value) => {
+ let search;
+ const locationSearch = window.location.search;
+
+ if (locationSearch.length) {
+ const parameters = locationSearch.substring(1, locationSearch.length)
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ // eslint-disable-next-line no-param-reassign
+ acc[val[0]] = decodeURIComponent(val[1]);
+ return acc;
+ }, {});
+
+ parameters[param] = value;
+
+ const toString = Object.keys(parameters)
+ .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
+ .join('&');
+
+ search = `?${toString}`;
+ } else {
+ search = `?${param}=${value}`;
+ }
+
+ return search;
+};
+
+/**
+ * Converts permission provided as strings to booleans.
+ *
+ * @param {String} string
+ * @returns {Boolean}
+ */
+export const convertPermissionToBoolean = permission => permission === 'true';
+
+/**
+ * Back Off exponential algorithm
+ * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
+ *
+ * @param {Function<next, stop>} fn function to be called
+ * @param {Number} timeout
+ * @return {Promise<Any, Error>}
+ * @example
+ * ```
+ * backOff(function (next, stop) {
+ * // Let's perform this function repeatedly for 60s or for the timeout provided.
+ *
+ * ourFunction()
+ * .then(function (result) {
+ * // continue if result is not what we need
+ * next();
+ *
+ * // when result is what we need let's stop with the repetions and jump out of the cycle
+ * stop(result);
+ * })
+ * .catch(function (error) {
+ * // if there is an error, we need to stop this with an error.
+ * stop(error);
+ * })
+ * }, 60000)
+ * .then(function (result) {})
+ * .catch(function (error) {
+ * // deal with errors passed to stop()
+ * })
+ * ```
+ */
+export const backOff = (fn, timeout = 60000) => {
+ const maxInterval = 32000;
+ let nextInterval = 2000;
+ let timeElapsed = 0;
+
+ return new Promise((resolve, reject) => {
+ const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+
+ const next = () => {
+ if (timeElapsed < timeout) {
+ setTimeout(() => fn(next, stop), nextInterval);
+ timeElapsed += nextInterval;
+ nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else {
- return val;
- }
- };
-
- gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
- return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
- };
-
- w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
- event_name = event_name || 'input';
- var closest_submit, field, that;
- that = this;
- field = $(field_selector);
- closest_submit = field.closest('form').find(button_selector);
- if (this.rstrip(field.val()) === "") {
- closest_submit.disable();
+ reject(new Error('BACKOFF_TIMEOUT'));
}
- return field.on(event_name, function() {
- if (that.rstrip($(this).val()) === "") {
- return closest_submit.disable();
- } else {
- return closest_submit.enable();
- }
- });
};
- // automatically adjust scroll position for hash urls taking the height of the navbar into account
- // https://github.com/twitter/bootstrap/issues/1768
- w.gl.utils.handleLocationHash = function() {
- var hash = w.gl.utils.getLocationHash();
- if (!hash) return;
-
- // This is required to handle non-unicode characters in hash
- hash = decodeURIComponent(hash);
-
- const fixedTabs = document.querySelector('.js-tabs-affix');
- const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
- const fixedNav = document.querySelector('.navbar-gitlab');
-
- var adjustment = 0;
- if (fixedNav) adjustment -= fixedNav.offsetHeight;
-
- // scroll to user-generated markdown anchor if we cannot find a match
- if (document.getElementById(hash) === null) {
- var target = document.getElementById('user-content-' + hash);
- if (target && target.scrollIntoView) {
- target.scrollIntoView(true);
- window.scrollBy(0, adjustment);
- }
+ fn(next, stop);
+ });
+};
+
+export const setFavicon = (faviconPath) => {
+ const faviconEl = document.getElementById('favicon');
+ if (faviconEl && faviconPath) {
+ faviconEl.setAttribute('href', faviconPath);
+ }
+};
+
+export const resetFavicon = () => {
+ const faviconEl = document.getElementById('favicon');
+ const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
+ if (faviconEl) {
+ faviconEl.setAttribute('href', originalFavicon);
+ }
+};
+
+export const setCiStatusFavicon = (pageUrl) => {
+ $.ajax({
+ url: pageUrl,
+ dataType: 'json',
+ success: (data) => {
+ if (data && data.favicon) {
+ setFavicon(data.favicon);
} else {
- // only adjust for fixedTabs when not targeting user-generated content
- if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
- }
-
- if (fixedDiffStats) {
- adjustment -= fixedDiffStats.offsetHeight;
- }
-
- window.scrollBy(0, adjustment);
+ resetFavicon();
}
- };
-
- // Check if element scrolled into viewport from above or below
- // Courtesy http://stackoverflow.com/a/7557433/414749
- w.gl.utils.isInViewport = function(el) {
- var rect = el.getBoundingClientRect();
-
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= window.innerHeight &&
- rect.right <= window.innerWidth
- );
- };
-
- gl.utils.getPagePath = function(index) {
- index = index || 0;
- return $('body').data('page').split(':')[index];
- };
-
- gl.utils.parseUrl = function (url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- };
-
- gl.utils.parseUrlPathname = function (url) {
- var parsedUrl = gl.utils.parseUrl(url);
- // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
- // We have to make sure we always have an absolute path.
- return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
- };
-
- gl.utils.getUrlParamsArray = function () {
- // We can trust that each param has one & since values containing & will be encoded
- // Remove the first character of search as it is always ?
- return window.location.search.slice(1).split('&').map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
- });
- };
-
- gl.utils.isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
- gl.utils.isMetaClick = function(e) {
- // Identify following special clicks
- // 1) Cmd + Click on Mac (e.metaKey)
- // 2) Ctrl + Click on PC (e.ctrlKey)
- // 3) Middle-click or Mouse Wheel Click (e.which is 2)
- return e.metaKey || e.ctrlKey || e.which === 2;
- };
-
- gl.utils.scrollToElement = function($el) {
- const top = $el.offset().top;
- const mrTabsHeight = $('.merge-request-tabs').height() || 0;
- const headerHeight = $('.navbar-gitlab').height() || 0;
-
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
- };
-
- /**
- this will take in the `name` of the param you want to parse in the url
- if the name does not exist this function will return `null`
- otherwise it will return the value of the param key provided
- */
- w.gl.utils.getParameterByName = (name, parseUrl) => {
- const url = parseUrl || window.location.href;
- name = name.replace(/[[\]]/g, '\\$&');
- const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
- const results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeURIComponent(results[2].replace(/\+/g, ' '));
- };
-
- w.gl.utils.getSelectedFragment = () => {
- const selection = window.getSelection();
- if (selection.rangeCount === 0) return null;
- 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;
- };
-
- w.gl.utils.insertText = (target, text) => {
- // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
-
- const selectionStart = target.selectionStart;
- const selectionEnd = target.selectionEnd;
- const value = target.value;
-
- const textBefore = value.substring(0, selectionStart);
- const textAfter = value.substring(selectionEnd, value.length);
-
- const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
- const newText = textBefore + insertedText + textAfter;
-
- target.value = newText;
- target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
-
- // Trigger autosave
- $(target).trigger('input');
-
- // Trigger autosize
- var event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- target.dispatchEvent(event);
- };
-
- w.gl.utils.nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
- Element.prototype.matchesSelector ||
- Element.prototype.mozMatchesSelector ||
- Element.prototype.msMatchesSelector ||
- Element.prototype.oMatchesSelector ||
- Element.prototype.webkitMatchesSelector;
-
- if (matches) {
- return matches.call(node, selector);
- }
-
- // IE11 doesn't support `node.matches(selector)`
-
- let parentNode = node.parentNode;
- if (!parentNode) {
- parentNode = document.createElement('div');
- node = node.cloneNode(true);
- parentNode.appendChild(node);
- }
-
- const matchingNodes = parentNode.querySelectorAll(selector);
- return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
- };
-
- /**
- this will take in the headers from an API response and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
- */
- w.gl.utils.normalizeHeaders = (headers) => {
- const upperCaseHeaders = {};
-
- Object.keys(headers).forEach((e) => {
- upperCaseHeaders[e.toUpperCase()] = headers[e];
- });
-
- return upperCaseHeaders;
- };
-
- /**
- this will take in the getAllResponseHeaders result and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
- */
- w.gl.utils.normalizeCRLFHeaders = (headers) => {
- const headersObject = {};
- const headersArray = headers.split('\n');
-
- headersArray.forEach((header) => {
- const keyValue = header.split(': ');
- headersObject[keyValue[0]] = keyValue[1];
- });
-
- return w.gl.utils.normalizeHeaders(headersObject);
- };
-
- /**
- * Parses pagination object string values into numbers.
- *
- * @param {Object} paginationInformation
- * @returns {Object}
- */
- w.gl.utils.parseIntPagination = paginationInformation => ({
- perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
- page: parseInt(paginationInformation['X-PAGE'], 10),
- total: parseInt(paginationInformation['X-TOTAL'], 10),
- totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
- nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
- previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
- });
-
- /**
- * Updates the search parameter of a URL given the parameter and value provided.
- *
- * If no search params are present we'll add it.
- * If param for page is already present, we'll update it
- * If there are params but not for the given one, we'll add it at the end.
- * Returns the new search parameters.
- *
- * @param {String} param
- * @param {Number|String|Undefined|Null} value
- * @return {String}
- */
- w.gl.utils.setParamInURL = (param, value) => {
- let search;
- const locationSearch = window.location.search;
-
- if (locationSearch.length) {
- const parameters = locationSearch.substring(1, locationSearch.length)
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- acc[val[0]] = decodeURIComponent(val[1]);
- return acc;
- }, {});
-
- parameters[param] = value;
-
- const toString = Object.keys(parameters)
- .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
- .join('&');
-
- search = `?${toString}`;
- } else {
- search = `?${param}=${value}`;
- }
-
- return search;
- };
-
- /**
- * Converts permission provided as strings to booleans.
- *
- * @param {String} string
- * @returns {Boolean}
- */
- w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
-
- /**
- * Back Off exponential algorithm
- * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
- *
- * @param {Function<next, stop>} fn function to be called
- * @param {Number} timeout
- * @return {Promise<Any, Error>}
- * @example
- * ```
- * backOff(function (next, stop) {
- * // Let's perform this function repeatedly for 60s or for the timeout provided.
- *
- * ourFunction()
- * .then(function (result) {
- * // continue if result is not what we need
- * next();
- *
- * // when result is what we need let's stop with the repetions and jump out of the cycle
- * stop(result);
- * })
- * .catch(function (error) {
- * // if there is an error, we need to stop this with an error.
- * stop(error);
- * })
- * }, 60000)
- * .then(function (result) {})
- * .catch(function (error) {
- * // deal with errors passed to stop()
- * })
- * ```
- */
- w.gl.utils.backOff = (fn, timeout = 60000) => {
- const maxInterval = 32000;
- let nextInterval = 2000;
- let timeElapsed = 0;
-
- return new Promise((resolve, reject) => {
- const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
-
- const next = () => {
- if (timeElapsed < timeout) {
- setTimeout(() => fn(next, stop), nextInterval);
- timeElapsed += nextInterval;
- nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
- } else {
- reject(new Error('BACKOFF_TIMEOUT'));
- }
- };
-
- fn(next, stop);
- });
- };
-
- w.gl.utils.setFavicon = (faviconPath) => {
- if (faviconEl && faviconPath) {
- faviconEl.setAttribute('href', faviconPath);
- }
- };
-
- w.gl.utils.resetFavicon = () => {
- if (faviconEl) {
- faviconEl.setAttribute('href', originalFavicon);
- }
- };
-
- w.gl.utils.setCiStatusFavicon = (pageUrl) => {
- $.ajax({
- url: pageUrl,
- dataType: 'json',
- success: function(data) {
- if (data && data.favicon) {
- gl.utils.setFavicon(data.favicon);
- } else {
- gl.utils.resetFavicon();
- }
- },
- error: function() {
- gl.utils.resetFavicon();
- }
- });
- };
- })(window);
-}).call(window);
+ },
+ error: () => {
+ resetFavicon();
+ },
+ });
+};
+
+export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+
+export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
+
+window.gl = window.gl || {};
+window.gl.utils = {
+ ...(window.gl.utils || {}),
+ getPagePath,
+ isInGroupsPage,
+ isInProjectPage,
+ getProjectSlug,
+ getGroupSlug,
+ isInIssuePage,
+ ajaxGet,
+ ajaxPost,
+ rstrip,
+ updateTooltipTitle,
+ disableButtonIfEmptyField,
+ handleLocationHash,
+ isInViewport,
+ parseUrl,
+ parseUrlPathname,
+ getUrlParamsArray,
+ isMetaKey,
+ isMetaClick,
+ scrollToElement,
+ getParameterByName,
+ getSelectedFragment,
+ insertText,
+ nodeMatchesSelector,
+ spriteIcon,
+ imagePath,
+};
diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js
new file mode 100644
index 00000000000..0bdb547d31a
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/csrf.js
@@ -0,0 +1,58 @@
+/*
+This module provides easy access to the CSRF token and caches
+it for re-use. It also exposes some values commonly used in relation
+to the CSRF token (header key and headers object).
+
+If you need to refresh the csrfToken for some reason, just call `init` and
+then use the accessors as you would normally.
+
+If you need to compose a headers object, use the spread operator:
+
+```
+ headers: {
+ ...csrf.headers,
+ someOtherHeader: '12345',
+ }
+```
+
+see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
+and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62
+ */
+
+const csrf = {
+ init() {
+ const tokenEl = document.querySelector('meta[name=csrf-token]');
+
+ if (tokenEl !== null) {
+ this.csrfToken = tokenEl.getAttribute('content');
+ } else {
+ this.csrfToken = null;
+ }
+ },
+
+ get token() {
+ return this.csrfToken;
+ },
+
+ get headerKey() {
+ return 'X-CSRF-Token';
+ },
+
+ get headers() {
+ if (this.csrfToken !== null) {
+ return {
+ [this.headerKey]: this.token,
+ };
+ }
+ return {};
+ },
+};
+
+csrf.init();
+
+// use our cached token for any $.rails-generated AJAX requests
+if ($.rails) {
+ $.rails.csrfToken = () => csrf.token;
+}
+
+export default csrf;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 1d1763c3963..29fc91733b3 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -55,7 +55,7 @@ window.dateFormat = dateFormat;
if (!timeagoInstance) {
const localeRemaining = function(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
@@ -73,7 +73,7 @@ window.dateFormat = dateFormat;
};
locale = function(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js
new file mode 100644
index 00000000000..2977ec821cb
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/image_utility.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+
+export function isImageLoaded(element) {
+ return element.complete && element.naturalHeight !== 0;
+}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 57394097944..917a45eb06b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
- digitsLeft = number.split('.')[0];
+ digitsLeft = number.toString().split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 97666e13ebe..1485e900945 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -1,4 +1,5 @@
import httpStatusCodes from './http_status';
+import { normalizeHeaders } from './common_utils';
/**
* Polling utility for handling realtime updates.
@@ -57,7 +58,7 @@ export default class Poll {
}
checkConditions(response) {
- const headers = gl.utils.normalizeHeaders(response.headers);
+ const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js
index 227bf65b560..b1ffd797f7e 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js
+++ b/app/assets/javascripts/lib/utils/pretty_time.js
@@ -1,68 +1,61 @@
import _ from 'underscore';
-(() => {
- /*
- * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
- * non-condensed, abbreviateTimelengths)
- * */
-
- const utils = window.gl.utils = gl.utils || {};
- const prettyTime = utils.prettyTime = {
- /*
- * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
- * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
- * or week length.
- */
- parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
- const DAYS_PER_WEEK = daysPerWeek;
- const HOURS_PER_DAY = hoursPerDay;
- const MINUTES_PER_HOUR = 60;
- const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
- const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
-
- const timePeriodConstraints = {
- weeks: MINUTES_PER_WEEK,
- days: MINUTES_PER_DAY,
- hours: MINUTES_PER_HOUR,
- minutes: 1,
- };
+/*
+ * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
+ * non-condensed, abbreviateTimelengths)
+ * */
+
+/*
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
+ * or week length.
+*/
+
+export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
+ const DAYS_PER_WEEK = daysPerWeek;
+ const HOURS_PER_DAY = hoursPerDay;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
- let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
+ let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
- return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
- const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+ return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
- unorderedMinutes -= (periodCount * minutesPerPeriod);
+ unorderedMinutes -= (periodCount * minutesPerPeriod);
- return periodCount;
- });
- },
+ return periodCount;
+ });
+}
- /*
- * Accepts a timeObject and returns a condensed string representation of it
- * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
- */
+/*
+* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
+* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+*/
- stringifyTime(timeObject) {
- const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
- return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
- }, '').trim();
- return reducedTime.length ? reducedTime : '0m';
- },
+export function stringifyTime(timeObject) {
+ const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
+ const isNonZero = !!unitValue;
+ return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
+ }, '').trim();
+ return reducedTime.length ? reducedTime : '0m';
+}
- /*
- * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
- * the first non-zero unit/value pair.
- */
+/*
+* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+* the first non-zero unit/value pair.
+*/
- abbreviateTime(timeStr) {
- return timeStr.split(' ')
- .filter(unitStr => unitStr.charAt(0) !== '0')[0];
- },
+export function abbreviateTime(timeStr) {
+ return timeStr.split(' ')
+ .filter(unitStr => unitStr.charAt(0) !== '0')[0];
+}
- secondsToMinutes(seconds) {
- return Math.abs(seconds / 60);
- },
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 283c0ec0410..64db42701ce 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,14 +1,34 @@
-export const isSticky = (el, scrollY, stickyTop) => {
+export const createPlaceholder = () => {
+ const placeholder = document.createElement('div');
+ placeholder.classList.add('sticky-placeholder');
+
+ return placeholder;
+};
+
+export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
const top = Math.floor(el.offsetTop - scrollY);
- if (top <= stickyTop) {
+ if (top <= stickyTop && !el.classList.contains('is-stuck')) {
+ const placeholder = insertPlaceholder ? createPlaceholder() : null;
+ const heightBefore = el.offsetHeight;
+
el.classList.add('is-stuck');
- } else {
+
+ if (insertPlaceholder) {
+ el.parentNode.insertBefore(placeholder, el.nextElementSibling);
+
+ placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
+ }
+ } else if (top > stickyTop && el.classList.contains('is-stuck')) {
el.classList.remove('is-stuck');
+
+ if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) {
+ el.nextElementSibling.remove();
+ }
}
};
-export default (el) => {
+export default (el, insertPlaceholder = true) => {
if (!el) return;
const computedStyle = window.getComputedStyle(el);
@@ -17,7 +37,7 @@ export default (el) => {
const stickyTop = parseInt(computedStyle.top, 10);
- document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), {
+ document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,
});
};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 3328ff9cc23..78c7a094127 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
+
var base;
var w = window;
if (w.gl == null) {
@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) {
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
-w.gl.utils.visitUrl = (url) => {
- document.location.href = url;
+// eslint-disable-next-line import/prefer-default-export
+export function visitUrl(url, external = false) {
+ if (external) {
+ // Simulate `target="blank" rel="noopener noreferrer"`
+ // See https://mathiasbynens.github.io/rel-noopener/
+ const otherWindow = window.open();
+ otherWindow.opener = null;
+ otherWindow.location = url;
+ } else {
+ document.location.href = url;
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.utils = {
+ ...(window.gl.utils || {}),
+ visitUrl,
};
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 7400c22543f..a75d1a4b8d0 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -28,148 +28,151 @@
// </div>
// </div>
//
-(function() {
- this.LineHighlighter = (function() {
- // CSS class applied to highlighted lines
- LineHighlighter.prototype.highlightClass = 'hll';
-
- // Internal copy of location.hash so we're not dependent on `location` in tests
- LineHighlighter.prototype._hash = '';
-
- function LineHighlighter(hash) {
- if (hash == null) {
- // Initialize a LineHighlighter object
- //
- // hash - String URL hash for dependency injection in tests
- hash = location.hash;
- }
- this.setHash = this.setHash.bind(this);
- this.highlightLine = this.highlightLine.bind(this);
- this.clickHandler = this.clickHandler.bind(this);
- this.highlightHash = this.highlightHash.bind(this);
- this._hash = hash;
- this.bindEvents();
- 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], {
- // Scroll to the first highlighted line on initial load
- // Offset -50 for the sticky top bar, and another -100 for some context
- offset: -150
- });
- }
- }
- };
-
- LineHighlighter.prototype.clickHandler = function(event) {
- var current, lineNumber, range;
- event.preventDefault();
- this.clearHighlight();
- lineNumber = $(event.target).closest('a').data('line-number');
- current = this.hashToRange(this._hash);
- if (!(current[0] && event.shiftKey)) {
- // If there's no current selection, or there is but Shift wasn't held,
- // treat this like a single-line selection.
- this.setHash(lineNumber);
- return this.highlightLine(lineNumber);
- } else if (event.shiftKey) {
- if (lineNumber < current[0]) {
- range = [lineNumber, current[0]];
- } else {
- range = [current[0], lineNumber];
- }
- this.setHash(range[0], range[1]);
- return this.highlightRange(range);
- }
- };
-
- LineHighlighter.prototype.clearHighlight = function() {
- return $("." + this.highlightClass).removeClass(this.highlightClass);
- // Unhighlight previously highlighted lines
- };
-
- // Convert a URL hash String into line numbers
- //
- // hash - Hash String
- //
- // Examples:
- //
- // hashToRange('#L5') # => [5, null]
- // hashToRange('#L5-15') # => [5, 15]
- // hashToRange('#foo') # => [null, null]
- //
- // Returns an Array
- LineHighlighter.prototype.hashToRange = function(hash) {
- var first, last, matches;
- // ?L(\d+)(?:-(\d+))?$/)
- matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
- if (matches && matches.length) {
- first = parseInt(matches[1], 10);
- last = matches[2] ? parseInt(matches[2], 10) : null;
- return [first, last];
- } else {
- return [null, null];
- }
- };
-
- // Highlight a single line
- //
- // lineNumber - Line number to highlight
- LineHighlighter.prototype.highlightLine = function(lineNumber) {
- return $("#LC" + lineNumber).addClass(this.highlightClass);
- };
-
- // Highlight all lines within a range
- //
- // range - Array containing the starting and ending line numbers
- LineHighlighter.prototype.highlightRange = function(range) {
- var i, lineNumber, ref, ref1, results;
- if (range[1]) {
- results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
- results.push(this.highlightLine(lineNumber));
- }
- return results;
- } else {
- return this.highlightLine(range[0]);
- }
- };
+const LineHighlighter = function(options = {}) {
+ options.highlightLineClass = options.highlightLineClass || 'hll';
+ options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
+ options.scrollFileHolder = options.scrollFileHolder || false;
+ options.hash = options.hash || location.hash;
- // Set the URL hash string
- LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
- var hash;
- if (lastLineNumber) {
- hash = "#L" + firstLineNumber + "-" + lastLineNumber;
+ this.options = options;
+ this._hash = options.hash;
+ this.highlightLineClass = options.highlightLineClass;
+ this.setHash = this.setHash.bind(this);
+ this.highlightLine = this.highlightLine.bind(this);
+ this.clickHandler = this.clickHandler.bind(this);
+ this.highlightHash = this.highlightHash.bind(this);
+
+ this.bindEvents();
+ this.highlightHash();
+};
+
+LineHighlighter.prototype.bindEvents = function() {
+ const $fileHolder = $(this.options.fileHolderSelector);
+
+ $fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
+ $fileHolder.on('highlight:line', this.highlightHash);
+};
+
+LineHighlighter.prototype.highlightHash = function(newHash) {
+ let range;
+ if (newHash && typeof newHash === 'string') this._hash = newHash;
+
+ this.clearHighlight();
+
+ if (this._hash !== '') {
+ range = this.hashToRange(this._hash);
+ if (range[0]) {
+ this.highlightRange(range);
+ const lineSelector = `#L${range[0]}`;
+ const scrollOptions = {
+ // Scroll to the first highlighted line on initial load
+ // Offset -50 for the sticky top bar, and another -100 for some context
+ offset: -150
+ };
+ if (this.options.scrollFileHolder) {
+ $(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions);
} else {
- hash = "#L" + firstLineNumber;
+ $.scrollTo(lineSelector, scrollOptions);
}
- this._hash = hash;
- return this.__setLocationHash__(hash);
- };
-
- // Make the actual hash change in the browser
- //
- // This method is stubbed in tests.
- LineHighlighter.prototype.__setLocationHash__ = function(value) {
- return history.pushState({
- url: value
- // We're using pushState instead of assigning location.hash directly to
- // prevent the page from scrolling on the hashchange event
- }, document.title, value);
- };
-
- return LineHighlighter;
- })();
-}).call(window);
+ }
+ }
+};
+
+LineHighlighter.prototype.clickHandler = function(event) {
+ var current, lineNumber, range;
+ event.preventDefault();
+ this.clearHighlight();
+ lineNumber = $(event.target).closest('a').data('line-number');
+ current = this.hashToRange(this._hash);
+ if (!(current[0] && event.shiftKey)) {
+ // If there's no current selection, or there is but Shift wasn't held,
+ // treat this like a single-line selection.
+ this.setHash(lineNumber);
+ return this.highlightLine(lineNumber);
+ } else if (event.shiftKey) {
+ if (lineNumber < current[0]) {
+ range = [lineNumber, current[0]];
+ } else {
+ range = [current[0], lineNumber];
+ }
+ this.setHash(range[0], range[1]);
+ return this.highlightRange(range);
+ }
+};
+
+LineHighlighter.prototype.clearHighlight = function() {
+ return $("." + this.highlightLineClass).removeClass(this.highlightLineClass);
+};
+
+// Convert a URL hash String into line numbers
+//
+// hash - Hash String
+//
+// Examples:
+//
+// hashToRange('#L5') # => [5, null]
+// hashToRange('#L5-15') # => [5, 15]
+// hashToRange('#foo') # => [null, null]
+//
+// Returns an Array
+LineHighlighter.prototype.hashToRange = function(hash) {
+ var first, last, matches;
+ // ?L(\d+)(?:-(\d+))?$/)
+ matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
+ if (matches && matches.length) {
+ first = parseInt(matches[1], 10);
+ last = matches[2] ? parseInt(matches[2], 10) : null;
+ return [first, last];
+ } else {
+ return [null, null];
+ }
+};
+
+// Highlight a single line
+//
+// lineNumber - Line number to highlight
+LineHighlighter.prototype.highlightLine = function(lineNumber) {
+ return $("#LC" + lineNumber).addClass(this.highlightLineClass);
+};
+
+// Highlight all lines within a range
+//
+// range - Array containing the starting and ending line numbers
+LineHighlighter.prototype.highlightRange = function(range) {
+ var i, lineNumber, ref, ref1, results;
+ if (range[1]) {
+ results = [];
+ for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ results.push(this.highlightLine(lineNumber));
+ }
+ return results;
+ } else {
+ return this.highlightLine(range[0]);
+ }
+};
+
+// Set the URL hash string
+LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
+ var hash;
+ if (lastLineNumber) {
+ hash = "#L" + firstLineNumber + "-" + lastLineNumber;
+ } else {
+ hash = "#L" + firstLineNumber;
+ }
+ this._hash = hash;
+ return this.__setLocationHash__(hash);
+};
+
+// Make the actual hash change in the browser
+//
+// This method is stubbed in tests.
+LineHighlighter.prototype.__setLocationHash__ = function(value) {
+ return history.pushState({
+ url: value
+ // We're using pushState instead of assigning location.hash directly to
+ // prevent the page from scrolling on the hashchange event
+ }, document.title, value);
+};
+
+window.LineHighlighter = LineHighlighter;
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 7ba676d6d20..1003b9ba0af 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,29 +1,13 @@
import Jed from 'jed';
+import sprintf from './sprintf';
-/**
- This is required to require all the translation folders in the current directory
- this saves us having to do this manually & keep up to date with new languages
-**/
-function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
-
-const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
-const locales = allLocales.reduce((d, obj) => {
- const data = d;
- const localeKey = Object.keys(obj)[0];
-
- data[localeKey] = obj[localeKey];
-
- return data;
-}, {});
-
-let lang = document.querySelector('html').getAttribute('lang') || 'en';
-lang = lang.replace(/-/g, '_');
-
-const locale = new Jed(locales[lang]);
+const langAttribute = document.querySelector('html').getAttribute('lang');
+const lang = (langAttribute || 'en').replace(/-/g, '_');
+const locale = new Jed(window.translations || {});
+delete window.translations;
/**
Translates `text`
-
@param text The text to be translated
@returns {String} The translated text
**/
@@ -67,4 +51,5 @@ export { lang };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
+export { sprintf };
export default locale;
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
new file mode 100644
index 00000000000..5f4a053f98e
--- /dev/null
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -0,0 +1,26 @@
+import _ from 'underscore';
+
+/**
+ Very limited implementation of sprintf supporting only named parameters.
+
+ @param input (translated) text with parameters (e.g. '%{num_users} users use us')
+ @param parameters object mapping parameter names to values (e.g. { num_users: 5 })
+ @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
+ @returns {String} the text with parameters replaces (e.g. '5 users use us')
+
+ @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
+ @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
+**/
+export default (input, parameters, escapeParameters = true) => {
+ let output = input;
+
+ if (parameters) {
+ Object.keys(parameters).forEach((parameterName) => {
+ const parameterValue = parameters[parameterName];
+ const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue;
+ output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue);
+ });
+ }
+
+ return output;
+};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 0bc31a56684..853546b617b 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -8,6 +8,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import Dropzone from 'dropzone';
import Sortable from 'vendor/Sortable';
+import svg4everybody from 'svg4everybody';
// libraries with import side-effects
import 'mousetrap';
@@ -34,14 +35,10 @@ import './shortcuts_network';
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
-// commit
-import './commit/file';
import './commit/image_file';
// lib/utils
-import './lib/utils/animate';
-import './lib/utils/bootstrap_linked_tabs';
-import './lib/utils/common_utils';
+import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
import './lib/utils/text_utility';
@@ -57,10 +54,8 @@ import './u2f/register';
import './u2f/util';
// everything else
-import './abuse_reports';
import './activities';
import './admin';
-import './ajax_loading_spinner';
import './api';
import './aside';
import './autosave';
@@ -71,14 +66,12 @@ import './build';
import './build_artifacts';
import './build_variables';
import './ci_lint_editor';
-import './commit';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
-import './create_label';
import './diff';
import './dropzone_input';
import './due_date_select';
@@ -102,7 +95,6 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
-import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
@@ -112,7 +104,6 @@ import './merge_request';
import './merge_request_tabs';
import './milestone';
import './milestone_select';
-import './mini_pipeline_graph_dropdown';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
@@ -120,12 +111,10 @@ import './notes';
import './notifications_dropdown';
import './notifications_form';
import './pager';
-import './pipelines';
import './preview_markdown';
import './project';
import './project_avatar';
import './project_find_file';
-import './project_fork';
import './project_import';
import './project_label_subscription';
import './project_new';
@@ -153,6 +142,8 @@ if (process.env.NODE_ENV !== 'production') require('./test_utils/');
Dropzone.autoDiscover = false;
+svg4everybody();
+
document.addEventListener('beforeunload', function () {
// Unbind scroll events
$(document).off('scroll');
@@ -162,10 +153,10 @@ document.addEventListener('beforeunload', function () {
$('[data-toggle="popover"]').popover('destroy');
});
-window.addEventListener('hashchange', gl.utils.handleLocationHash);
+window.addEventListener('hashchange', handleLocationHash);
window.addEventListener('load', function onLoad() {
window.removeEventListener('load', onLoad, false);
- gl.utils.handleLocationHash();
+ handleLocationHash();
}, false);
gl.lazyLoader = new LazyLoader({
@@ -191,7 +182,7 @@ $(function () {
$body.on('click', 'a[href^="#"]', function() {
var href = this.getAttribute('href');
if (href.substr(1) === gl.utils.getLocationHash()) {
- setTimeout(gl.utils.handleLocationHash, 1);
+ setTimeout(handleLocationHash, 1);
}
});
@@ -301,7 +292,10 @@ $(function () {
return $container.remove();
// Commit show suppressed diff
});
- $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded'));
+ $('.navbar-toggle').on('click', () => {
+ $('.header-content').toggleClass('menu-expanded');
+ gl.lazyLoader.loadCheck();
+ });
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 0db2abe507d..af0658eb668 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper';
$el.text(gl.text.addDelimiter(count));
};
+ MergeRequest.prototype.hideCloseButton = function() {
+ const el = document.querySelector('.merge-request .issuable-actions');
+ const closeDropdownItem = el.querySelector('li.close-item');
+ if (closeDropdownItem) {
+ closeDropdownItem.classList.add('hidden');
+ // Selects the next dropdown item
+ el.querySelector('li.report-item').click();
+ } else {
+ // No dropdown just hide the Close button
+ el.querySelector('.btn-close').classList.add('hidden');
+ }
+ // Dropdown for mobile screen
+ el.querySelector('li.js-close-item').classList.add('hidden');
+ };
+
return MergeRequest;
})();
}).call(window);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 0c3c877ff15..c042b22d1fd 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -7,6 +7,13 @@ import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
+import {
+ parseUrlPathname,
+ handleLocationHash,
+ isMetaClick,
+} from './lib/utils/common_utils';
+
+import initDiscussionTab from './image_diff/init_discussion_tab';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -114,7 +121,7 @@ import bp from './breakpoints';
}
clickTab(e) {
- if (e.currentTarget && gl.utils.isMetaClick(e)) {
+ if (e.currentTarget && isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation();
e.preventDefault();
@@ -149,6 +156,8 @@ import bp from './breakpoints';
}
this.resetViewContainer();
this.destroyPipelinesView();
+
+ initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
@@ -243,6 +252,8 @@ import bp from './breakpoints';
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
@@ -260,7 +271,7 @@ import bp from './breakpoints';
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
- const urlPathname = gl.utils.parseUrlPathname(source);
+ const urlPathname = parseUrlPathname(source);
this.ajaxGet({
url: `${urlPathname}.json${location.search}`,
@@ -309,7 +320,7 @@ import bp from './breakpoints';
forceShow: true,
});
anchor[0].scrollIntoView();
- window.gl.utils.handleLocationHash();
+ handleLocationHash();
// We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
@@ -345,7 +356,7 @@ import bp from './breakpoints';
}
expandViewContainer() {
- const $wrapper = $('.content-wrapper .container-fluid');
+ const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 04579058688..4675b1fcb8f 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -45,7 +45,7 @@ import _ from 'underscore';
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
+ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -208,6 +208,7 @@ import _ from 'underscore';
if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
+ data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index b596c4f383f..442ed86d50c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,13 +1,13 @@
<script>
/* global Flash */
import _ from 'underscore';
- import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
+ import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
export default {
@@ -18,15 +18,18 @@
return {
store,
state: 'gettingStarted',
- hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
+ hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
- endpoint: metricsData.additionalMetrics,
+ metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
+ emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
+ emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath,
+ emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath,
showEmptyState: true,
- backOffRequestCounter: 0,
updateAspectRatio: false,
updatedAspectRatios: 0,
+ hoverData: {},
resizeThrottled: {},
};
},
@@ -39,50 +42,16 @@
methods: {
getGraphsData() {
- const maxNumberOfRequests = 3;
this.state = 'loading';
- gl.utils.backOff((next, stop) => {
- this.service.get().then((resp) => {
- if (resp.status === statusCodes.NO_CONTENT) {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
- if (this.backOffRequestCounter < maxNumberOfRequests) {
- next();
- } else {
- stop(new Error('Failed to connect to the prometheus server'));
- }
- } else {
- stop(resp);
- }
- }).catch(stop);
- })
- .then((resp) => {
- if (resp.status === statusCodes.NO_CONTENT) {
- this.state = 'unableToConnect';
- return false;
- }
- return resp.json();
- })
- .then((metricGroupsData) => {
- if (!metricGroupsData) return false;
- this.store.storeMetrics(metricGroupsData.data);
- return this.getDeploymentData();
- })
- .then((deploymentData) => {
- if (deploymentData !== false) {
- this.store.storeDeploymentData(deploymentData.deployments);
- this.showEmptyState = false;
- }
- return {};
- })
- .catch(() => {
- this.state = 'unableToConnect';
- });
- },
-
- getDeploymentData() {
- return this.service.getDeploymentData(this.deploymentEndpoint)
- .then(resp => resp.json())
- .catch(() => new Flash('Error getting deployment information.'));
+ Promise.all([
+ this.service.getGraphsData()
+ .then(data => this.store.storeMetrics(data)),
+ this.service.getDeploymentData()
+ .then(data => this.store.storeDeploymentData(data))
+ .catch(() => new Flash('Error getting deployment information.')),
+ ])
+ .then(() => { this.showEmptyState = false; })
+ .catch(() => { this.state = 'unableToConnect'; });
},
resize() {
@@ -96,15 +65,24 @@
this.updatedAspectRatios = 0;
}
},
+
+ hoverChanged(data) {
+ this.hoverData = data;
+ },
},
created() {
- this.service = new MonitoringService(this.endpoint);
+ this.service = new MonitoringService({
+ metricsEndpoint: this.metricsEndpoint,
+ deploymentEndpoint: this.deploymentEndpoint,
+ });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
},
@@ -131,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics"
:key="index"
:graph-data="graphData"
+ :hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
@@ -141,5 +120,8 @@
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
+ :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
+ :empty-loading-svg-path="emptyLoadingSvgPath"
+ :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
/>
</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index a8708be76de..a18164482a2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,8 +1,4 @@
<script>
- import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg';
- import loadingSvg from 'empty_states/monitoring/_loading.svg';
- import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg';
-
export default {
props: {
documentationPath: {
@@ -18,24 +14,36 @@
type: String,
required: true,
},
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
states: {
gettingStarted: {
- svg: gettingStartedSvg,
+ svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.',
buttonText: 'Configure Prometheus',
},
loading: {
- svg: loadingSvg,
+ svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data',
description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.',
buttonText: 'View documentation',
},
unableToConnect: {
- svg: unableToConnectSvg,
+ svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
@@ -65,32 +73,22 @@
<template>
<div class="prometheus-state">
- <div class="row">
- <div class="col-md-4 col-md-offset-4 state-svg" v-html="currentState.svg"></div>
- </div>
- <div class="row">
- <div class="col-md-6 col-md-offset-3">
- <h4 class="text-center state-title">
- {{currentState.title}}
- </h4>
- </div>
- </div>
- <div class="row">
- <div class="col-md-6 col-md-offset-3">
- <div class="description-text text-center state-description">
- {{currentState.description}}
- <a v-if="showButtonDescription" :href="settingsPath">
- Prometheus server
- </a>
- </div>
- </div>
+ <div class="state-svg svg-content">
+ <img :src="currentState.svgUrl"/>
</div>
- <div class="row state-button-section">
- <div class="col-md-4 col-md-offset-4 text-center state-button">
- <a class="btn btn-success" :href="buttonPath">
- {{currentState.buttonText}}
- </a>
- </div>
+ <h4 class="state-title">
+ {{currentState.title}}
+ </h4>
+ <p class="state-description">
+ {{currentState.description}}
+ <a v-if="showButtonDescription" :href="settingsPath">
+ Prometheus server
+ </a>
+ </p>
+ <div class="state-button">
+ <a class="btn btn-success" :href="buttonPath">
+ {{currentState.buttonText}}
+ </a>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index cde2ff7ca2a..5aa3865f96a 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -3,16 +3,14 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
- import monitoringPaths from './monitoring_paths.vue';
+ import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
- import { timeScaleFormat } from '../utils/date_time_formatters';
+ import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
- const bisectDate = d3.bisector(d => d.time).left;
-
export default {
props: {
graphData: {
@@ -27,6 +25,11 @@
type: Array,
required: true,
},
+ hoverData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
mixins: [MonitoringMixin],
@@ -40,8 +43,6 @@
graphHeightOffset: 120,
margin: {},
unitOfDisplay: '',
- areaColorRgb: '#8fbce8',
- lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
@@ -54,6 +55,7 @@
currentXCoordinate: 0,
currentFlagPosition: 0,
showFlag: false,
+ showFlagContent: false,
showDeployInfo: true,
timeSeries: [],
};
@@ -63,11 +65,11 @@
GraphLegend,
GraphFlag,
GraphDeployment,
- monitoringPaths,
+ GraphPath,
},
computed: {
- outterViewBox() {
+ outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
@@ -124,36 +126,30 @@
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- this.currentData = evalTime ? d1 : d0;
- this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
- this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
+ const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
+ const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
-
- if (currentDeployXPos) {
- this.showFlag = false;
- } else {
- this.showFlag = true;
- }
+ eventHub.$emit('hoverChanged', {
+ hoveredDate,
+ currentDeployXPos,
+ });
},
renderAxesPaths() {
- this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset);
+ this.timeSeries = createTimeSeries(
+ this.graphData.queries[0],
+ this.graphWidth,
+ this.graphHeight,
+ this.graphHeightOffset,
+ );
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale()
- .range([0, this.graphWidth]);
+ .range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
@@ -162,7 +158,7 @@
const xAxis = d3.svg.axis()
.scale(axisXScale)
- .ticks(measurements.xTicks)
+ .ticks(d3.time.minute, 60)
.tickFormat(timeScaleFormat)
.orient('bottom');
@@ -196,6 +192,10 @@
eventHub.$emit('toggleAspectRatio');
}
},
+
+ hoverData() {
+ this.positionFlag();
+ },
},
mounted() {
@@ -205,7 +205,10 @@
</script>
<template>
- <div class="prometheus-graph">
+ <div
+ class="prometheus-graph"
+ @mouseover="showFlagContent = true"
+ @mouseleave="showFlagContent = false">
<h5 class="text-center graph-title">
{{graphData.title}}
</h5>
@@ -213,7 +216,7 @@
class="prometheus-svg-container"
:style="paddingBottomRootSvg">
<svg
- :viewBox="outterViewBox"
+ :viewBox="outerViewBox"
ref="baseSvg">
<g
class="x-axis"
@@ -238,7 +241,7 @@
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
- <monitoring-paths
+ <graph-path
v-for="(path, index) in timeSeries"
:key="index"
:generated-line-path="path.linePath"
@@ -246,9 +249,10 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
- <monitoring-deployment
+ <graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
+ :graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
@@ -259,6 +263,7 @@
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
+ :show-flag-content="showFlagContent"
/>
<rect
class="prometheus-graph-overlay"
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 3623d2ed946..e3b8be0c7fb 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -19,6 +19,10 @@
type: Number,
required: true,
},
+ graphWidth: {
+ type: Number,
+ required: true,
+ },
},
computed: {
@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
+
+ positionFlag(deployment) {
+ let xPosition = 3;
+ if (deployment.xPos > (this.graphWidth - 200)) {
+ xPosition = -97;
+ }
+ return xPosition;
+ },
},
};
</script>
@@ -77,7 +89,7 @@
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
- x="3"
+ :x="positionFlag(deployment)"
y="0"
width="92"
height="60">
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index a98e3d06c18..10fb7ff6803 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -23,6 +23,10 @@
type: Number,
required: true,
},
+ showFlagContent: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
@@ -57,6 +61,7 @@
transform="translate(-5, 20)">
</line>
<svg
+ v-if="showFlagContent"
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index a43dad8e601..85b6d7f4cbe 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -79,7 +79,18 @@
},
formatMetricUsage(series) {
- return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
+ const value = series.values[this.currentDataIndex].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
+ },
+
+ createSeriesString(index, series) {
+ if (series.metricTag) {
+ return `${series.metricTag} ${this.formatMetricUsage(series)}`;
+ }
+ return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
},
mounted() {
@@ -164,7 +175,7 @@
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
- {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
+ {{createSeriesString(index, series)}}
</text>
<text
v-else
diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..043f1bf66bb 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_paths.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 345a0b37a76..31f38aca5d6 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -1,3 +1,5 @@
+import { bisectDate } from '../utils/date_time_formatters';
+
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
@@ -18,6 +20,7 @@ const mixins = {
return dataFound;
},
+
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray;
}, []);
},
+
+ positionFlag() {
+ const timeSeries = this.timeSeries[0];
+ const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+ this.currentData = timeSeries.values[hoveredDataIndex];
+ this.currentDataIndex = hoveredDataIndex;
+ this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
+ if (this.currentXCoordinate > (this.graphWidth - 200)) {
+ this.currentFlagPosition = this.currentXCoordinate - 103;
+ } else {
+ this.currentFlagPosition = this.currentXCoordinate;
+ }
+
+ if (this.hoverData.currentDeployXPos) {
+ this.showFlag = false;
+ } else {
+ this.showFlag = true;
+ }
+ },
},
};
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index ef280e02092..104432ef5de 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
- components: {
- Dashboard,
- },
- render: createElement => createElement('dashboard'),
+ render: createElement => createElement(Dashboard),
}));
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 1e9ae934853..fed884d5c94 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,19 +1,55 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
+import statusCodes from '../../lib/utils/http_status';
+import { backOff } from '../../lib/utils/common_utils';
Vue.use(VueResource);
+const MAX_REQUESTS = 3;
+
+function backOffRequest(makeRequestCallback) {
+ let requestCounter = 0;
+ return backOff((next, stop) => {
+ makeRequestCallback().then((resp) => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ requestCounter += 1;
+ if (requestCounter < MAX_REQUESTS) {
+ next();
+ } else {
+ stop(new Error('Failed to connect to the prometheus server'));
+ }
+ } else {
+ stop(resp);
+ }
+ }).catch(stop);
+ });
+}
+
export default class MonitoringService {
- constructor(endpoint) {
- this.graphs = Vue.resource(endpoint);
+ constructor({ metricsEndpoint, deploymentEndpoint }) {
+ this.metricsEndpoint = metricsEndpoint;
+ this.deploymentEndpoint = deploymentEndpoint;
}
- get() {
- return this.graphs.get();
+ getGraphsData() {
+ return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
+ .then(resp => resp.json())
+ .then((response) => {
+ if (!response || !response.data) {
+ throw new Error('Unexpected metrics data response from prometheus endpoint');
+ }
+ return response.data;
+ });
}
- // eslint-disable-next-line class-methods-use-this
- getDeploymentData(endpoint) {
- return Vue.http.get(endpoint);
+ getDeploymentData() {
+ return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
+ .then(resp => resp.json())
+ .then((response) => {
+ if (!response || !response.deployments) {
+ throw new Error('Unexpected deployment data response from prometheus endpoint');
+ }
+ return response.deployments;
+ });
}
}
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 7592af5878e..854636e9a89 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
- value,
+ value: Number(value),
})),
})),
})),
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index 26bcaa02511..c4c6b1ac1f5 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p');
+export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()],
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 05d551e917c..65eec0d8d02 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,8 +1,37 @@
import d3 from 'd3';
import _ from 'underscore';
-export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
- const maxValues = seriesData.map((timeSeries, index) => {
+const defaultColorPalette = {
+ blue: ['#1f78d1', '#8fbce8'],
+ orange: ['#fc9403', '#feca81'],
+ red: ['#db3b21', '#ed9d90'],
+ green: ['#1aaa55', '#8dd5aa'],
+ purple: ['#6666c4', '#d1d1f0'],
+};
+
+const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
+
+export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
+ let usedColors = [];
+
+ function pickColor(name) {
+ let pick;
+ if (name && defaultColorPalette[name]) {
+ pick = name;
+ } else {
+ const unusedColors = _.difference(defaultColorOrder, usedColors);
+ if (unusedColors.length > 0) {
+ pick = unusedColors[0];
+ } else {
+ usedColors = [];
+ pick = defaultColorOrder[0];
+ }
+ }
+ usedColors.push(pick);
+ return defaultColorPalette[pick];
+ }
+
+ const maxValues = queryData.result.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
@@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
- let timeSeriesNumber = 1;
- let lineColor = '#1f78d1';
- let areaColor = '#8fbce8';
- return seriesData.map((timeSeries) => {
+ return queryData.result.map((timeSeries, timeSeriesNumber) => {
+ let metricTag = '';
+ let lineColor = '';
+ let areaColor = '';
+
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
@@ -23,49 +53,34 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+ const defined = d => !isNaN(d.value) && d.value != null;
+
const lineFunction = d3.svg.line()
+ .defined(defined)
+ .interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
+ .defined(defined)
+ .interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value))
- .interpolate('linear');
-
- switch (timeSeriesNumber) {
- case 1:
- lineColor = '#1f78d1';
- areaColor = '#8fbce8';
- break;
- case 2:
- lineColor = '#fc9403';
- areaColor = '#feca81';
- break;
- case 3:
- lineColor = '#db3b21';
- areaColor = '#ed9d90';
- break;
- case 4:
- lineColor = '#1aaa55';
- areaColor = '#8dd5aa';
- break;
- case 5:
- lineColor = '#6666c4';
- areaColor = '#d1d1f0';
- break;
- default:
- lineColor = '#1f78d1';
- areaColor = '#8fbce8';
- break;
- }
+ .y1(d => timeSeriesScaleY(d.value));
- if (timeSeriesNumber <= 5) {
- timeSeriesNumber = timeSeriesNumber += 1;
+ const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
+ const seriesCustomizationData = queryData.series != null &&
+ _.findWhere(queryData.series[0].when,
+ { value: timeSeriesMetricLabel });
+ if (seriesCustomizationData != null) {
+ metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
+ [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
- timeSeriesNumber = 1;
+ metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`;
+ [lineColor, areaColor] = pickColor();
}
return {
@@ -75,6 +90,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
values: timeSeries.values,
lineColor,
areaColor,
+ metricTag,
};
});
}
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index 709a5d33b9f..997550b37fb 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -11,14 +11,19 @@ export default class NewNavSidebar {
initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar');
+ this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
- this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
}
bindEvents() {
+ document.addEventListener('click', (e) => {
+ if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
+ this.toggleCollapsedSidebar(true);
+ }
+ });
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
@@ -52,9 +57,15 @@ export default class NewNavSidebar {
}
NewNavSidebar.setCollapsedCookie(collapsed);
- this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
- return collapsed ? this.getAttribute('aria-label') : '';
- });
+ this.toggleSidebarOverflow();
+ }
+
+ toggleSidebarOverflow() {
+ if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
+ this.$innerScroll.css('overflow-y', 'scroll');
+ } else {
+ this.$innerScroll.css('overflow-y', '');
+ }
}
render() {
@@ -63,7 +74,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') {
- const collapse = this.$sidebar.hasClass('sidebar-icons-only');
+ const collapse = Cookies.get('sidebar_collapsed') === 'true';
this.toggleCollapsedSidebar(collapse);
}
}
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index b8a16356576..b4067d229aa 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -1,18 +1,3 @@
-<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';
@@ -51,6 +36,21 @@ export default {
};
</script>
+<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>
+
<style scoped>
.cell {
flex-direction: column;
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index 31b30f601e2..0f3083f05b2 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -1,17 +1,3 @@
-<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';
@@ -55,3 +41,17 @@
},
};
</script>
+
+<template>
+ <div :class="type">
+ <prompt
+ :type="promptType"
+ :count="count" />
+ <pre
+ class="language-python"
+ :class="codeCssClass"
+ ref="code"
+ v-text="code">
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 814d2ea92b4..82c51a1068c 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,10 +1,3 @@
-<template>
- <div class="cell text-cell">
- <prompt />
- <div class="markdown" v-html="markdown"></div>
- </div>
-</template>
-
<script>
/* global katex */
import marked from 'marked';
@@ -95,6 +88,13 @@
};
</script>
+<template>
+ <div class="cell text-cell">
+ <prompt />
+ <div class="markdown" v-html="markdown"></div>
+ </div>
+</template>
+
<style>
.markdown .katex {
display: block;
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 0f39cd138df..2110a9de7ed 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,10 +1,3 @@
-<template>
- <div class="output">
- <prompt />
- <div v-html="rawCode"></div>
- </div>
-</template>
-
<script>
import Prompt from '../prompt.vue';
@@ -20,3 +13,10 @@ export default {
},
};
</script>
+
+<template>
+ <div class="output">
+ <prompt />
+ <div v-html="rawCode"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue
index f3b873bbc0f..fbb39ea6e2d 100644
--- a/app/assets/javascripts/notebook/cells/output/image.vue
+++ b/app/assets/javascripts/notebook/cells/output/image.vue
@@ -1,11 +1,3 @@
-<template>
- <div class="output">
- <prompt />
- <img
- :src="'data:' + outputType + ';base64,' + rawCode" />
- </div>
-</template>
-
<script>
import Prompt from '../prompt.vue';
@@ -25,3 +17,11 @@ export default {
},
};
</script>
+
+<template>
+ <div class="output">
+ <prompt />
+ <img
+ :src="'data:' + outputType + ';base64,' + rawCode" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 23c9ea78939..05af0bf1e8e 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -1,12 +1,3 @@
-<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';
@@ -81,3 +72,12 @@ export default {
},
};
</script>
+
+<template>
+ <component :is="componentName"
+ type="output"
+ :outputType="outputType"
+ :count="count"
+ :raw-code="rawCode"
+ :code-css-class="codeCssClass" />
+</template>
diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue
index 4540e4248d8..039fb99293d 100644
--- a/app/assets/javascripts/notebook/cells/prompt.vue
+++ b/app/assets/javascripts/notebook/cells/prompt.vue
@@ -1,11 +1,3 @@
-<template>
- <div class="prompt">
- <span v-if="type && count">
- {{ type }} [{{ count }}]:
- </span>
- </div>
-</template>
-
<script>
export default {
props: {
@@ -21,6 +13,14 @@
};
</script>
+<template>
+ <div class="prompt">
+ <span v-if="type && count">
+ {{ type }} [{{ count }}]:
+ </span>
+ </div>
+</template>
+
<style scoped>
.prompt {
padding: 0 10px;
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index fd62c1231ef..e88806431af 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -1,14 +1,3 @@
-<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,
@@ -59,6 +48,17 @@
};
</script>
+<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>
+
<style>
.cell,
.input,
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index a09270d6d24..24de21f2ce2 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -23,6 +23,8 @@ import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import TaskList from './task_list';
+import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
+import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize;
window.Dropzone = Dropzone;
@@ -41,6 +43,7 @@ export default class Notes {
this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.onAddDiffNote = this.onAddDiffNote.bind(this);
+ this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this);
@@ -81,7 +84,7 @@ export default class Notes {
this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab
- if (gl.utils.getPagePath(1) === 'merge_requests') {
+ if (getPagePath(1) === 'merge_requests') {
$('.note-edit-form').clone()
.addClass('mr-note-edit-form').insertAfter('.note-edit-form');
}
@@ -113,6 +116,8 @@ export default class Notes {
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
+ // add diff note for images
+ $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
@@ -139,6 +144,7 @@ export default class Notes {
$(document).off('click', '.js-note-attachment-delete');
$(document).off('click', '.js-discussion-reply-button');
$(document).off('click', '.js-add-diff-note-button');
+ $(document).off('click', '.js-add-image-diff-note-button');
$(document).off('visibilitychange');
$(document).off('keyup input', '.js-note-text');
$(document).off('click', '.js-note-target-reopen');
@@ -175,7 +181,7 @@ export default class Notes {
keydownNoteText(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
- if (gl.utils.isMetaKey(e)) {
+ if (isMetaKey(e)) {
return;
}
@@ -411,6 +417,11 @@ export default class Notes {
this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr');
+
+ if (noteEntity.on_image) {
+ row = form;
+ }
+
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
@@ -422,7 +433,7 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
@@ -448,6 +459,7 @@ export default class Notes {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
+
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
@@ -560,7 +572,7 @@ export default class Notes {
form.find('#note_line_code').val(),
// DiffNote
- form.find('#note_position').val()
+ form.find('#note_position').val(),
];
return new Autosave(textarea, key);
}
@@ -644,10 +656,10 @@ export default class Notes {
}
else {
var $buttons = $el.find('.note-form-actions');
- var isWidgetVisible = gl.utils.isInViewport($el.get(0));
+ var isWidgetVisible = isInViewport($el.get(0));
if (!isWidgetVisible) {
- gl.utils.scrollToElement($el);
+ scrollToElement($el);
}
$el.find('.js-finish-edit-warning').show();
@@ -782,9 +794,22 @@ export default class Notes {
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff
- if (notesTr.find('.discussion-notes').length > 1) {
+ // notesTr does not exist for image diffs
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
+ const $diffFile = $notes.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
+ },
+ });
+
+ $diffFile[0].dispatchEvent(removeBadgeEvent);
+ }
+
$notes.remove();
- } else {
+ } else if (notesTr.length > 0) {
notesTr.remove();
}
}
@@ -840,7 +865,11 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
- const diffFileData = dataHolder.closest('.text-file');
+ let diffFileData = dataHolder.closest('.text-file');
+
+ if (diffFileData.length === 0) {
+ diffFileData = dataHolder.closest('.image');
+ }
var discussionID = dataHolder.data('discussionId');
@@ -906,6 +935,31 @@ export default class Notes {
});
}
+ onAddImageDiffNote(e) {
+ const $link = $(e.currentTarget || e.target);
+ const $diffFile = $link.closest('.diff-file');
+
+ const clickEvent = new CustomEvent('click.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(clickEvent);
+
+ // Setup comment form
+ let newForm;
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
+ const $form = $noteContainer.find('> .discussion-form');
+
+ if ($form.length === 0) {
+ newForm = this.cleanForm(this.formClone.clone());
+ newForm.appendTo($noteContainer);
+ } else {
+ newForm = $form;
+ }
+
+ this.setupDiscussionNoteForm($link, newForm);
+ }
+
toggleDiffNote({
target,
lineType,
@@ -998,10 +1052,25 @@ export default class Notes {
}
cancelDiscussionForm(e) {
- var form;
e.preventDefault();
- form = $(e.target).closest('.js-discussion-note-form');
- return this.removeDiscussionNoteForm(form);
+ const $form = $(e.target).closest('.js-discussion-note-form');
+ const $discussionNote = $(e.target).closest('.discussion-notes');
+
+ if ($discussionNote.length === 0) {
+ // Only send blur event when the discussion form
+ // is not part of a discussion note
+ const $diffFile = $form.closest('.diff-file');
+
+ if ($diffFile.length > 0) {
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(blurEvent);
+ }
+ }
+
+ return this.removeDiscussionNoteForm($form);
}
/**
@@ -1188,7 +1257,7 @@ export default class Notes {
}
static checkMergeRequestStatus() {
- if (gl.utils.getPagePath(1) === 'merge_requests') {
+ if (getPagePath(1) === 'merge_requests') {
gl.mrWidget.checkStatus();
}
}
@@ -1272,16 +1341,16 @@ export default class Notes {
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
- <a href="/${currentUsername}">
+ <a href="/${_.escape(currentUsername)}">
<img class="avatar s40" src="${currentUserAvatar}" />
</a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
<div class="note-header-info">
- <a href="/${currentUsername}">
- <span class="hidden-xs">${currentUserFullname}</span>
- <span class="note-headline-light">@${currentUsername}</span>
+ <a href="/${_.escape(currentUsername)}">
+ <span class="hidden-xs">${_.escape(currentUsername)}</span>
+ <span class="note-headline-light">${_.escape(currentUsername)}</span>
</a>
</div>
</div>
@@ -1295,6 +1364,9 @@ export default class Notes {
</li>`
);
+ $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
+ $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
+
return $tempNote;
}
@@ -1323,7 +1395,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 4) Show placeholder note on UI
- * 5) Perform network request to submit the note using `gl.utils.ajaxPost`
+ * 5) Perform network request to submit the note using `ajaxPost`
* a) If request is successfully completed
* 1. Remove placeholder element
* 2. Show submitted Note element
@@ -1405,11 +1477,20 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- gl.utils.ajaxPost(formAction, formData)
+ ajaxPost(formAction, formData)
.then((note) => {
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
+ const $diffFile = $form.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(blurEvent);
+ }
+
// Reset cached commands list when command is applied
if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
@@ -1432,7 +1513,28 @@ export default class Notes {
}
// Show final note element on UI
- this.addDiscussionNote($form, note, $notesContainer.length === 0);
+ const isNewDiffComment = $notesContainer.length === 0;
+ this.addDiscussionNote($form, note, isNewDiffComment);
+
+ if (isNewDiffComment) {
+ // Add image badge, avatar badge and toggle discussion badge for new image diffs
+ const notePosition = $form.find('#note_position').val();
+ if ($diffFile.length > 0 && notePosition.length > 0) {
+ const { x, y, width, height } = JSON.parse(notePosition);
+ const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
+ detail: {
+ x,
+ y,
+ width,
+ height,
+ noteId: `note_${note.id}`,
+ discussionId: note.discussion_id,
+ },
+ });
+
+ $diffFile[0].dispatchEvent(addBadgeEvent);
+ }
+ }
// append flash-container to the Notes list
if ($notesContainer.length) {
@@ -1453,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ const closestDiffFile = $form.closest('.diff-file');
+
+ if (closestDiffFile.length) {
+ closestDiffFile[0].dispatchEvent(blurEvent);
+ }
+
if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove();
}
@@ -1478,7 +1590,7 @@ export default class Notes {
*
* 1) Get Form metadata
* 2) Update note element with new content
- * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost`
+ * 3) Perform network request to submit the updated note using `ajaxPost`
* a) If request is successfully completed
* 1. Show submitted Note element
* b) If request failed
@@ -1496,6 +1608,8 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form);
+ const $diffFile = $form.closest('.diff-file');
+ const $notesContainer = $form.closest('.notes');
// Cache original comment content
const cachedNoteBodyText = $noteBodyText.html();
@@ -1507,7 +1621,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
- gl.utils.ajaxPost(formAction, formData)
+ ajaxPost(formAction, formData)
.then((note) => {
// Submission successful! render final note element
this.updateNote(note, $editingNote);
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 16f4e22aa9b..ab8516296a8 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -2,14 +2,17 @@
/* global Flash, Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
+ import autosize from 'vendor/autosize';
import '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
- import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
+ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
+ import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueCommentForm',
@@ -25,8 +28,9 @@
};
},
components: {
- confidentialIssue,
+ issueWarning,
issueNoteSignedOutWidget,
+ issueDiscussionLockedWidget,
markdownField,
userAvatarLink,
},
@@ -54,6 +58,9 @@
isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
+ canCreateNote() {
+ return this.getIssueData.current_user.can_create_note;
+ },
issueActionButtonTitle() {
if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen';
@@ -89,13 +96,12 @@
endpoint() {
return this.getIssueData.create_note_path;
},
- isConfidentialIssue() {
- return this.getIssueData.confidential;
- },
},
methods: {
...mapActions([
'saveNote',
+ 'stopPolling',
+ 'restartPolling',
'removePlaceholderNotes',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
@@ -124,10 +130,14 @@
}
this.isSubmitting = true;
this.note = ''; // Empty textarea while being requested. Repopulate in catch
+ this.resizeTextarea();
+ this.stopPolling();
this.saveNote(noteData)
.then((res) => {
this.isSubmitting = false;
+ this.restartPolling();
+
if (res.errors) {
if (res.errors.commands_only) {
this.discard();
@@ -174,6 +184,8 @@
if (shouldClear) {
this.note = '';
+ this.resizeTextarea();
+ this.$refs.markdownField.previewMarkdown = false;
}
// reset autostave
@@ -205,7 +217,15 @@
selector: '.notes',
});
},
+ resizeTextarea() {
+ this.$nextTick(() => {
+ autosize.update(this.$refs.textarea);
+ });
+ },
},
+ mixins: [
+ issuableStateMixin,
+ ],
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
@@ -221,6 +241,7 @@
<template>
<div>
<issue-note-signed-out-widget v-if="!isLoggedIn" />
+ <issue-discussion-locked-widget v-else-if="!canCreateNote" />
<ul
v-else
class="notes notes-form timeline">
@@ -239,15 +260,23 @@
<div class="timeline-content timeline-content-form">
<form
ref="commentForm"
- class="new-note js-quick-submit common-note-form gfm-form js-main-target-form">
- <confidentialIssue v-if="isConfidentialIssue" />
+ class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
+ >
+
<div class="error-alert"></div>
+
+ <issue-warning
+ v-if="hasWarning(getIssueData)"
+ :is-locked="isLocked(getIssueData)"
+ :is-confidential="isConfidential(getIssueData)"
+ />
+
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
- :is-confidential-issue="isConfidentialIssue">
+ ref="markdownField">
<textarea
id="note-body"
name="note[note]"
@@ -257,6 +286,7 @@
v-model="note"
ref="textarea"
slot="textarea"
+ :disabled="isSubmitting"
placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()">
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
new file mode 100644
index 00000000000..e73ec2aaf71
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
@@ -0,0 +1,19 @@
+<script>
+ export default {
+ computed: {
+ lockIcon() {
+ return gl.utils.spriteIcon('lock');
+ },
+ },
+ };
+
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <span class="issuable-note-warning">
+ <span class="icon" v-html="lockIcon"></span>
+ <span>This issue is locked. Only <b>project members</b> can comment.</span>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 3483f6c7538..1f43b8a16ad 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -62,7 +62,7 @@
},
deleteHandler() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to delete this list?')) {
+ if (confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true;
this.deleteNote(this.note)
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue
index 60c172321d1..feb3e73194b 100644
--- a/app/assets/javascripts/notes/components/issue_note_actions.vue
+++ b/app/assets/javascripts/notes/components/issue_note_actions.vue
@@ -86,7 +86,7 @@
<div class="note-actions">
<span
v-if="accessLevel"
- class="note-role">{{accessLevel}}</span>
+ class="note-role note-role-access">{{accessLevel}}</span>
<div
v-if="canAddAwardEmoji"
class="note-actions-item">
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue
index 626c0f2ce18..e2539d6b89d 100644
--- a/app/assets/javascripts/notes/components/issue_note_form.vue
+++ b/app/assets/javascripts/notes/components/issue_note_form.vue
@@ -1,8 +1,9 @@
<script>
import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
- import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
+ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
+ import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueNoteForm',
@@ -39,12 +40,13 @@
};
},
components: {
- confidentialIssue,
+ issueWarning,
markdownField,
},
computed: {
...mapGetters([
'getDiscussionLastNote',
+ 'getIssueData',
'getIssueDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
@@ -67,9 +69,6 @@
isDisabled() {
return !this.note.length || this.isSubmitting;
},
- isConfidentialIssue() {
- return this.getIssueDataByProp('confidential');
- },
},
methods: {
handleUpdate() {
@@ -95,6 +94,9 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
},
},
+ mixins: [
+ issuableStateMixin,
+ ],
mounted() {
this.$refs.textarea.focus();
},
@@ -125,7 +127,13 @@
<div class="flash-container timeline-content"></div>
<form
class="edit-note common-note-form js-quick-submit gfm-form">
- <confidentialIssue v-if="isConfidentialIssue" />
+
+ <issue-warning
+ v-if="hasWarning(getIssueData)"
+ :is-locked="isLocked(getIssueData)"
+ :is-confidential="isConfidential(getIssueData)"
+ />
+
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/notes/components/issue_note_icons.js b/app/assets/javascripts/notes/components/issue_note_icons.js
deleted file mode 100644
index d8e3cb4bc01..00000000000
--- a/app/assets/javascripts/notes/components/issue_note_icons.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import iconArrowCircle from 'icons/_icon_arrow_circle_o_right.svg';
-import iconCheck from 'icons/_icon_check_square_o.svg';
-import iconClock from 'icons/_icon_clock_o.svg';
-import iconCodeFork from 'icons/_icon_code_fork.svg';
-import iconComment from 'icons/_icon_comment_o.svg';
-import iconCommit from 'icons/_icon_commit.svg';
-import iconEdit from 'icons/_icon_edit.svg';
-import iconEye from 'icons/_icon_eye.svg';
-import iconEyeSlash from 'icons/_icon_eye_slash.svg';
-import iconMerge from 'icons/_icon_merge.svg';
-import iconMerged from 'icons/_icon_merged.svg';
-import iconRandom from 'icons/_icon_random.svg';
-import iconClosed from 'icons/_icon_status_closed.svg';
-import iconStatusOpen from 'icons/_icon_status_open.svg';
-import iconStopwatch from 'icons/_icon_stopwatch.svg';
-import iconTags from 'icons/_icon_tags.svg';
-import iconUser from 'icons/_icon_user.svg';
-
-export default {
- icon_arrow_circle_o_right: iconArrowCircle,
- icon_check_square_o: iconCheck,
- icon_clock_o: iconClock,
- icon_code_fork: iconCodeFork,
- icon_comment_o: iconComment,
- icon_commit: iconCommit,
- icon_edit: iconEdit,
- icon_eye: iconEye,
- icon_eye_slash: iconEyeSlash,
- icon_merge: iconMerge,
- icon_merged: iconMerged,
- icon_random: iconRandom,
- icon_status_closed: iconClosed,
- icon_status_open: iconStatusOpen,
- icon_stopwatch: iconStopwatch,
- icon_tags: iconTags,
- icon_user: iconUser,
-};
diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue
index 5bb8f871b9d..0cfb6522e77 100644
--- a/app/assets/javascripts/notes/components/issue_system_note.vue
+++ b/app/assets/javascripts/notes/components/issue_system_note.vue
@@ -1,6 +1,5 @@
<script>
import { mapGetters } from 'vuex';
- import iconsMap from './issue_note_icons';
import issueNoteHeader from './issue_note_header.vue';
export default {
@@ -24,9 +23,9 @@
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
- },
- created() {
- this.svg = iconsMap[this.note.system_note_icon_name];
+ iconHtml() {
+ return gl.utils.spriteIcon(this.note.system_note_icon_name);
+ },
},
};
</script>
@@ -39,7 +38,7 @@
<div class="timeline-entry-inner">
<div
class="timeline-icon"
- v-html="svg">
+ v-html="iconHtml">
</div>
<div class="timeline-content">
<div class="note-header">
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
new file mode 100644
index 00000000000..97f3ea0d5de
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -0,0 +1,15 @@
+export default {
+ methods: {
+ isConfidential(issue) {
+ return !!issue.confidential;
+ },
+
+ isLocked(issue) {
+ return !!issue.discussion_locked;
+ },
+
+ hasWarning(issue) {
+ return this.isConfidential(issue) || this.isLocked(issue);
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 13cd74bfa1c..1a791039909 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -7,6 +7,7 @@ import * as constants from '../constants';
import service from '../services/issue_notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
+import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
@@ -186,6 +187,14 @@ export const poll = ({ commit, state, getters }) => {
});
};
+export const stopPolling = () => {
+ eTagPoll.stop();
+};
+
+export const restartPolling = () => {
+ eTagPoll.restart();
+};
+
export const fetchData = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
@@ -211,7 +220,7 @@ export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => {
};
export const scrollToNoteIfNeeded = (context, el) => {
- if (!gl.utils.isInViewport(el[0])) {
- gl.utils.scrollToElement(el);
+ if (!isInViewport(el[0])) {
+ scrollToElement(el);
}
};
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 3b2b2089d6e..c2a08f3d6fe 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -5,15 +5,19 @@ import * as constants from '../constants';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
- const noteData = {
- expanded: true,
- id: discussion_id,
- individual_note: !(type === constants.DISCUSSION_NOTE),
- notes: [note],
- reply_id: discussion_id,
- };
-
- state.notes.push(noteData);
+ const [exists] = state.notes.filter(n => n.id === note.discussion_id);
+
+ if (!exists) {
+ const noteData = {
+ expanded: true,
+ id: discussion_id,
+ individual_note: !(type === constants.DISCUSSION_NOTE),
+ notes: [note],
+ reply_id: discussion_id,
+ };
+
+ state.notes.push(noteData);
+ }
},
[types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 01110420cca..e3fc1e2fc2f 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,4 +1,4 @@
-import '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
(() => {
@@ -9,7 +9,7 @@ import '~/lib/utils/url_utility';
init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
this.limit = limit;
- this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
+ this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
this.prepareData = prepareData;
this.callback = callback;
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index b874e484d45..c8a2f778ee8 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,13 +1,3 @@
-<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 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
@@ -64,6 +54,16 @@
};
</script>
+<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>
+
<style>
.pdf-viewer {
background: url('./assets/img/bg.gif');
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index 7b74ee4eb2e..be38f7cc129 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -1,10 +1,3 @@
-<template>
- <canvas
- class="pdf-page"
- ref="canvas"
- :data-page="number" />
-</template>
-
<script>
export default {
props: {
@@ -48,6 +41,13 @@
};
</script>
+<template>
+ <canvas
+ class="pdf-page"
+ ref="canvas"
+ :data-page="number" />
+</template>
+
<style>
.pdf-page {
margin: 8px auto 0 auto;
diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
index 644efd10509..9e0e5cacb11 100644
--- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
+++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
@@ -1,3 +1,5 @@
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+
function insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
@@ -6,7 +8,7 @@ function insertRow($row) {
}
function removeRow($row) {
- const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted'));
+ const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js
index 26a36ad54d1..07abe714367 100644
--- a/app/assets/javascripts/pipelines.js
+++ b/app/assets/javascripts/pipelines.js
@@ -1,4 +1,5 @@
import LinkedTabs from './lib/utils/bootstrap_linked_tabs';
+import { setCiStatusFavicon } from './lib/utils/common_utils';
export default class Pipelines {
constructor(options = {}) {
@@ -8,7 +9,7 @@ export default class Pipelines {
}
if (options.pipelineStatusUrl) {
- gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
+ setCiStatusFavicon(options.pipelineStatusUrl);
}
}
}
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 3db64339a62..0eaac8dd64f 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,21 +1,24 @@
<script>
-import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
-
export default {
props: {
helpPagePath: {
type: String,
required: true,
},
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
- data: () => ({ pipelinesEmptyStateSVG }),
};
</script>
<template>
<div class="row empty-state js-empty-state">
<div class="col-xs-12">
- <div class="svg-content" v-html="pipelinesEmptyStateSVG" />
+ <div class="svg-content">
+ <img :src="emptyStateSvgPath"/>
+ </div>
</div>
<div class="col-xs-12 text-center">
diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
index 90cee68163e..012853b201d 100644
--- a/app/assets/javascripts/pipelines/components/error_state.vue
+++ b/app/assets/javascripts/pipelines/components/error_state.vue
@@ -1,15 +1,20 @@
<script>
-import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
-
export default {
- data: () => ({ pipelinesErrorStateSVG }),
+ props: {
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
};
</script>
<template>
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12">
- <div class="svg-content" v-html="pipelinesErrorStateSVG" />
+ <div class="svg-content">
+ <img :src="errorStateSvgPath"/>
+ </div>
</div>
<div class="col-xs-12 text-center">
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index f0b44dfa6d8..9da0aac50a1 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -28,8 +28,7 @@
popoverOptions() {
return {
html: true,
- delay: { hide: 600 },
- trigger: 'hover',
+ trigger: 'focus',
placement: 'top',
title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
@@ -73,8 +72,16 @@
:title="pipeline.yaml_errors">
yaml invalid
</span>
+ <span
+ v-if="pipeline.flags.failure_reason"
+ v-tooltip
+ class="js-pipeline-url-failure label label-danger"
+ :title="pipeline.failure_reason">
+ error
+ </span>
<a
v-if="pipeline.flags.auto_devops"
+ tabindex="0"
class="js-pipeline-url-autodevops label label-info autodevops-badge"
v-popover="popoverOptions"
role="button">
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 5e6d6b2fbdc..085bd20cefe 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -4,6 +4,7 @@
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
+ import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
export default {
props: {
@@ -26,6 +27,8 @@
return {
endpoint: pipelinesData.endpoint,
helpPagePath: pipelinesData.helpPagePath,
+ emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
+ errorStateSvgPath: pipelinesData.errorStateSvgPath,
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
@@ -44,10 +47,10 @@
},
computed: {
canCreatePipelineParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ return convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
- const scope = gl.utils.getParameterByName('scope');
+ const scope = getParameterByName('scope');
return scope === null ? 'all' : scope;
},
@@ -105,10 +108,10 @@
};
},
pageParameter() {
- return gl.utils.getParameterByName('page') || this.pagenum;
+ return getParameterByName('page') || this.pagenum;
},
scopeParameter() {
- return gl.utils.getParameterByName('scope') || this.apiScope;
+ return getParameterByName('scope') || this.apiScope;
},
},
created() {
@@ -122,7 +125,7 @@
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
+ const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
@@ -181,9 +184,13 @@
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
+ :empty-state-svg-path="emptyStateSvgPath"
/>
- <error-state v-if="shouldRenderErrorState" />
+ <error-state
+ v-if="shouldRenderErrorState"
+ :error-state-svg-path="errorStateSvgPath"
+ />
<div
class="blank-state blank-state-no-icon"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 01dfe51cc17..c4c63a52358 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -69,8 +69,7 @@
@click="onClickAction(action.path)"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
- <span v-html="playIconSvg"></span>
- <span>{{action.name}}</span>
+ {{action.name}}
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index b19bd509a00..751a20991af 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -39,11 +39,7 @@
rel="nofollow"
download
:href="artifact.path">
- <i
- class="fa fa-download"
- aria-hidden="true">
- </i>
- <span>Download {{artifact.name}} artifacts</span>
+ Download {{artifact.name}} artifacts
</a>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 9adc15e6266..e97f5632dc8 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -97,7 +97,7 @@ export default {
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
- .catch(() => new Flash('An error occured while making the request.'));
+ .catch(() => new Flash('An error occurred while making the request.'));
},
},
};
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index ffefe0192f2..651251d2623 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -1,3 +1,5 @@
+import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
+
export default class PipelinesStore {
constructor() {
this.state = {};
@@ -19,8 +21,8 @@ export default class PipelinesStore {
let paginationInfo;
if (Object.keys(pagination).length) {
- const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
- paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ const normalizedHeaders = normalizeHeaders(pagination);
+ paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
new file mode 100644
index 00000000000..b2b34cb83e1
--- /dev/null
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -0,0 +1,146 @@
+<script>
+ import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+ import { __, s__, sprintf } from '../../../locale';
+ import csrf from '../../../lib/utils/csrf';
+
+ export default {
+ props: {
+ actionUrl: {
+ type: String,
+ required: true,
+ },
+ confirmWithPassword: {
+ type: Boolean,
+ required: true,
+ },
+ username: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ enteredPassword: '',
+ enteredUsername: '',
+ isOpen: false,
+ };
+ },
+ components: {
+ popupDialog,
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ inputLabel() {
+ let confirmationValue;
+ if (this.confirmWithPassword) {
+ confirmationValue = __('password');
+ } else {
+ confirmationValue = __('username');
+ }
+
+ confirmationValue = `<code>${confirmationValue}</code>`;
+
+ return sprintf(
+ s__('Profiles|Type your %{confirmationValue} to confirm:'),
+ { confirmationValue },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`Profiles|
+You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
+Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
+ {
+ yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ canSubmit() {
+ if (this.confirmWithPassword) {
+ return this.enteredPassword !== '';
+ }
+
+ return this.enteredUsername === this.username;
+ },
+ onSubmit(status) {
+ if (status) {
+ if (!this.canSubmit()) {
+ return;
+ }
+
+ this.$refs.form.submit();
+ }
+
+ this.toggleOpen(false);
+ },
+ toggleOpen(isOpen) {
+ this.isOpen = isOpen;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <popup-dialog
+ v-if="isOpen"
+ :title="s__('Profiles|Delete your account?')"
+ :text="text"
+ :kind="`danger ${!canSubmit() && 'disabled'}`"
+ :primary-button-label="s__('Profiles|Delete account')"
+ @toggle="toggleOpen"
+ @submit="onSubmit">
+
+ <template slot="body" scope="props">
+ <p v-html="props.text"></p>
+
+ <form
+ ref="form"
+ :action="actionUrl"
+ method="post">
+
+ <input
+ type="hidden"
+ name="_method"
+ value="delete" />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken" />
+
+ <p id="input-label" v-html="inputLabel"></p>
+
+ <input
+ v-if="confirmWithPassword"
+ name="password"
+ class="form-control"
+ type="password"
+ v-model="enteredPassword"
+ aria-labelledby="input-label" />
+ <input
+ v-else
+ name="username"
+ class="form-control"
+ type="text"
+ v-model="enteredUsername"
+ aria-labelledby="input-label" />
+ </form>
+ </template>
+
+ </popup-dialog>
+
+ <button
+ type="button"
+ class="btn btn-danger"
+ @click="toggleOpen(true)">
+ {{ s__('Profiles|Delete account') }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
new file mode 100644
index 00000000000..635056e0eeb
--- /dev/null
+++ b/app/assets/javascripts/profile/account/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+import deleteAccountModal from './components/delete_account_modal.vue';
+
+const deleteAccountModalEl = document.getElementById('delete-account-modal');
+// eslint-disable-next-line no-new
+new Vue({
+ el: deleteAccountModalEl,
+ components: {
+ deleteAccountModal,
+ },
+ render(createElement) {
+ return createElement('delete-account-modal', {
+ props: {
+ actionUrl: deleteAccountModalEl.dataset.actionUrl,
+ confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
+ username: deleteAccountModalEl.dataset.username,
+ },
+ });
+ },
+});
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 291ae24aa68..4bdda611cfc 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -73,7 +73,8 @@ import _ from 'underscore';
aspectRatio: 1,
modal: true,
scalable: false,
- rotatable: false,
+ rotatable: true,
+ checkOrientation: true,
zoomable: true,
dragMode: 'move',
guides: false,
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 4ccea0624ee..3deb242bc1f 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,5 +1,6 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
/* global Flash */
+import { getPagePath } from '../lib/utils/common_utils';
((global) => {
class Profile {
@@ -93,7 +94,7 @@
return $title.val(comment[1]).change();
}
});
- if (global.utils.getPagePath() === 'profiles') {
+ if (getPagePath() === 'profiles') {
return new Profile();
}
});
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 47197db39d3..65d46fa9a73 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,13 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
-(function() {
- this.ProjectFork = (function() {
- function ProjectFork() {
- $('.fork-thumbnail a').on('click', function() {
- $('.fork-namespaces').hide();
- return $('.save-project-loader').show();
- });
- }
+export default () => {
+ $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
+ if ($(this).hasClass('disabled')) return false;
- return ProjectFork;
- })();
-}).call(window);
+ return $('.js-fork-content').toggle();
+ });
+};
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
index fad956b4c26..9cbd8f21f2a 100644
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -19,7 +19,7 @@ export default class ProjectsService {
getSearchedProjects(searchQuery) {
return this.projectsPath.get({
- simple: false,
+ simple: true,
per_page: 20,
membership: !!gon.current_user_id,
order_by: 'last_activity_at',
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index ef4d6df5138..a4d50a52315 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -1,4 +1,5 @@
import PANEL_STATE from './constants';
+import { backOff } from '../lib/utils/common_utils';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
@@ -79,7 +80,7 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
- gl.utils.backOff((next, stop) => {
+ backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint)
.done((res) => {
if (res && res.success) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 10da3783123..0a9fdb074e5 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,15 +1,22 @@
+import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown';
+import AccessorUtilities from '../lib/utils/accessor';
+
+const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
export default class ProtectedBranchCreate {
constructor() {
this.$form = $('.js-new-protected-branch');
+ this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.currentProjectUserDefaults = {};
this.buildDropdowns();
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
+ const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
});
- // Select default
- $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
- $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
-
// Protected branch dropdown
this.protectedBranchDropdown = new ProtectedBranchDropdown({
- $dropdown: this.$form.find('.js-protected-branch-select'),
+ $dropdown: $protectedBranchDropdown,
onSelect: this.onSelectCallback,
});
+
+ this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
}
// This will run after clicked callback
@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate {
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+ const completedForm = !(
+ $branchInput.val() &&
+ $allowedToMergeInput.length &&
+ $allowedToPushInput.length
+ );
+
+ this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
+ this.$form.find('input[type="submit"]').attr('disabled', completedForm);
+ }
+
+ loadPreviousSelection(mergeDropdown, pushDropdown) {
+ let mergeIndex = 0;
+ let pushIndex = 0;
+ if (this.isLocalStorageAvailable) {
+ const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
+ if (savedDefaults != null) {
+ mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
+ id: parseInt(savedDefaults.mergeSelection, 0),
+ });
+ pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
+ id: parseInt(savedDefaults.pushSelection, 0),
+ });
+ }
+ }
+ mergeDropdown.selectRowAtIndex(mergeIndex);
+ pushDropdown.selectRowAtIndex(pushIndex);
+ }
- this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
+ savePreviousSelection(mergeSelection, pushSelection) {
+ if (this.isLocalStorageAvailable) {
+ const branchDefaults = {
+ mergeSelection,
+ pushSelection,
+ };
+ window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
+ }
}
}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
new file mode 100644
index 00000000000..2d8ca443ea7
--- /dev/null
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -0,0 +1,62 @@
+<script>
+ /* globals Flash */
+ import { mapGetters, mapActions } from 'vuex';
+ import '../../flash';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import store from '../stores';
+ import collapsibleContainer from './collapsible_container.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'registryListApp',
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ store,
+ components: {
+ collapsibleContainer,
+ loadingIcon,
+ },
+ computed: {
+ ...mapGetters([
+ 'isLoading',
+ 'repos',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'setMainEndpoint',
+ 'fetchRepos',
+ ]),
+ },
+ created() {
+ this.setMainEndpoint(this.endpoint);
+ },
+ mounted() {
+ this.fetchRepos()
+ .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ },
+ };
+</script>
+<template>
+ <div>
+ <loading-icon
+ v-if="isLoading"
+ size="3"
+ />
+
+ <collapsible-container
+ v-else-if="!isLoading && repos.length"
+ v-for="(item, index) in repos"
+ :key="index"
+ :repo="item"
+ />
+
+ <p v-else-if="!isLoading && !repos.length">
+ {{__("No container images stored for this project. Add one by following the instructions above.")}}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
new file mode 100644
index 00000000000..41ea9742406
--- /dev/null
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -0,0 +1,131 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import tableRegistry from './table_registry.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'collapsibeContainerRegisty',
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ loadingIcon,
+ tableRegistry,
+ },
+ directives: {
+ tooltip,
+ },
+ data() {
+ return {
+ isOpen: false,
+ };
+ },
+ computed: {
+ clipboardText() {
+ return `docker pull ${this.repo.location}`;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchRepos',
+ 'fetchList',
+ 'deleteRepo',
+ ]),
+
+ toggleRepo() {
+ this.isOpen = !this.isOpen;
+
+ if (this.isOpen) {
+ this.fetchList({ repo: this.repo })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ }
+ },
+
+ handleDeleteRepository() {
+ this.deleteRepo(this.repo)
+ .then(() => this.fetchRepos())
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="container-image">
+ <div
+ class="container-image-head">
+ <button
+ type="button"
+ @click="toggleRepo"
+ class="js-toggle-repo btn-link">
+ <i
+ class="fa"
+ :class="{
+ 'fa-chevron-right': !isOpen,
+ 'fa-chevron-up': isOpen,
+ }"
+ aria-hidden="true">
+ </i>
+ {{repo.name}}
+ </button>
+
+ <clipboard-button
+ v-if="repo.location"
+ :text="clipboardText"
+ :title="repo.location"
+ />
+
+ <div class="controls hidden-xs pull-right">
+ <button
+ v-if="repo.canDelete"
+ type="button"
+ class="js-remove-repo btn btn-danger"
+ :title="s__('ContainerRegistry|Remove repository')"
+ :aria-label="s__('ContainerRegistry|Remove repository')"
+ v-tooltip
+ @click="handleDeleteRepository">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </div>
+
+ </div>
+
+ <loading-icon
+ v-if="repo.isLoading"
+ class="append-bottom-20"
+ size="2"
+ />
+
+ <div
+ v-else-if="!repo.isLoading && isOpen"
+ class="container-image-tags">
+
+ <table-registry
+ v-if="repo.list.length"
+ :repo="repo"
+ />
+
+ <div
+ v-else
+ class="nothing-here-block">
+ {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
new file mode 100644
index 00000000000..4ce1571b0aa
--- /dev/null
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -0,0 +1,137 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import { n__ } from '../../locale';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import timeagoMixin from '../../vue_shared/mixins/timeago';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ tablePagination,
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ directives: {
+ tooltip,
+ },
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchList',
+ 'deleteRegistry',
+ ]),
+
+ layers(item) {
+ return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
+ },
+
+ handleDeleteRegistry(registry) {
+ this.deleteRegistry(registry)
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ },
+
+ onPageChange(pageNumber) {
+ this.fetchList({ repo: this.repo, page: pageNumber })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ },
+
+ clipboardText(text) {
+ return `docker pull ${text}`;
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+<template>
+<div>
+ <table class="table tags">
+ <thead>
+ <tr>
+ <th>{{s__('ContainerRegistry|Tag')}}</th>
+ <th>{{s__('ContainerRegistry|Tag ID')}}</th>
+ <th>{{s__("ContainerRegistry|Size")}}</th>
+ <th>{{s__("ContainerRegistry|Created")}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, i) in repo.list"
+ :key="i">
+ <td>
+
+ {{item.tag}}
+
+ <clipboard-button
+ v-if="item.location"
+ :title="item.location"
+ :text="clipboardText(item.location)"
+ />
+ </td>
+ <td>
+ <span
+ v-tooltip
+ :title="item.revision"
+ data-placement="bottom">
+ {{item.shortRevision}}
+ </span>
+ </td>
+ <td>
+ {{item.size}}
+ <template v-if="item.size && item.layers">
+ &middot;
+ </template>
+ {{layers(item)}}
+ </td>
+
+ <td>
+ {{timeFormated(item.createdAt)}}
+ </td>
+
+ <td class="content">
+ <button
+ v-if="item.canDelete"
+ type="button"
+ class="js-delete-registry btn btn-danger hidden-xs pull-right"
+ :title="s__('ContainerRegistry|Remove tag')"
+ :aria-label="s__('ContainerRegistry|Remove tag')"
+ data-container="body"
+ v-tooltip
+ @click="handleDeleteRegistry(item)">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onPageChange"
+ :page-info="repo.pagination"
+ />
+</div>
+</template>
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js
new file mode 100644
index 00000000000..712b0fade3d
--- /dev/null
+++ b/app/assets/javascripts/registry/constants.js
@@ -0,0 +1,15 @@
+import { __ } from '../locale';
+
+export const errorMessagesTypes = {
+ FETCH_REGISTRY: 'FETCH_REGISTRY',
+ FETCH_REPOS: 'FETCH_REPOS',
+ DELETE_REPO: 'DELETE_REPO',
+ DELETE_REGISTRY: 'DELETE_REGISTRY',
+};
+
+export const errorMessages = {
+ [errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
+ [errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
+ [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
+ [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
+};
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
new file mode 100644
index 00000000000..d8edff73f72
--- /dev/null
+++ b/app/assets/javascripts/registry/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import registryApp from './components/app.vue';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#js-vue-registry-images',
+ components: {
+ registryApp,
+ },
+ data() {
+ const dataset = document.querySelector(this.$options.el).dataset;
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('registry-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
new file mode 100644
index 00000000000..34ed40b8b65
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import * as types from './mutation_types';
+
+Vue.use(VueResource);
+
+export const fetchRepos = ({ commit, state }) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+
+ return Vue.http.get(state.endpoint)
+ .then(res => res.json())
+ .then((response) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+ commit(types.SET_REPOS_LIST, response);
+ });
+};
+
+export const fetchList = ({ commit }, { repo, page }) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+
+ return Vue.http.get(repo.tagsPath, { params: { page } })
+ .then((response) => {
+ const headers = response.headers;
+
+ return response.json().then((resp) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
+ });
+ });
+};
+
+export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
+ .then(res => res.json());
+
+export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
+ .then(res => res.json());
+
+export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
+export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
new file mode 100644
index 00000000000..588f479c492
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -0,0 +1,2 @@
+export const isLoading = state => state.isLoading;
+export const repos = state => state.repos;
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js
new file mode 100644
index 00000000000..78b67881210
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: {
+ isLoading: false,
+ endpoint: '', // initial endpoint to fetch the repos list
+ /**
+ * Each object in `repos` has the following strucure:
+ * {
+ * name: String,
+ * isLoading: Boolean,
+ * tagsPath: String // endpoint to request the list
+ * destroyPath: String // endpoit to delete the repo
+ * list: Array // List of the registry images
+ * }
+ *
+ * Each registry image inside `list` has the following structure:
+ * {
+ * tag: String,
+ * revision: String
+ * shortRevision: String
+ * size: Number
+ * layers: Number
+ * createdAt: String
+ * destroyPath: String // endpoit to delete each image
+ * }
+ */
+ repos: [],
+ },
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js
new file mode 100644
index 00000000000..2c69bf11807
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutation_types.js
@@ -0,0 +1,7 @@
+export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
+
+export const SET_REPOS_LIST = 'SET_REPOS_LIST';
+export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
+
+export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
+export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
new file mode 100644
index 00000000000..e40382e7afc
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -0,0 +1,54 @@
+import * as types from './mutation_types';
+import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
+
+export default {
+
+ [types.SET_MAIN_ENDPOINT](state, endpoint) {
+ Object.assign(state, { endpoint });
+ },
+
+ [types.SET_REPOS_LIST](state, list) {
+ Object.assign(state, {
+ repos: list.map(el => ({
+ canDelete: !!el.destroy_path,
+ destroyPath: el.destroy_path,
+ id: el.id,
+ isLoading: false,
+ list: [],
+ location: el.location,
+ name: el.path,
+ tagsPath: el.tags_path,
+ })),
+ });
+ },
+
+ [types.TOGGLE_MAIN_LOADING](state) {
+ Object.assign(state, { isLoading: !state.isLoading });
+ },
+
+ [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
+ const listToUpdate = state.repos.find(el => el.id === repo.id);
+
+ const normalizedHeaders = normalizeHeaders(headers);
+ const pagination = parseIntPagination(normalizedHeaders);
+
+ listToUpdate.pagination = pagination;
+
+ listToUpdate.list = resp.map(element => ({
+ tag: element.name,
+ revision: element.revision,
+ shortRevision: element.short_revision,
+ size: element.size,
+ layers: element.layers,
+ location: element.location,
+ createdAt: element.created_at,
+ destroyPath: element.destroy_path,
+ canDelete: !!element.destroy_path,
+ }));
+ },
+
+ [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
+ const listToUpdate = state.repos.find(el => el.id === list.id);
+ listToUpdate.isLoading = !listToUpdate.isLoading;
+ },
+};
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index d6c864cb976..cc60aa5939c 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -62,7 +62,7 @@ export default {
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
- :body="__('Are you sure you want to discard your changes?')"
+ :text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 1282828b504..119e38c583d 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -37,14 +37,14 @@ export default {
content: f.newContent,
}));
const payload = {
- branch: Store.targetBranch,
+ branch: Store.currentBranch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
Service.commitFiles(payload)
.then(this.resetCommitState)
- .catch(() => Flash('An error occured while committing your changes'));
+ .catch(() => Flash('An error occurred while committing your changes'));
},
resetCommitState() {
@@ -105,7 +105,7 @@ export default {
</label>
<div class="col-md-6">
<span class="help-block">
- {{targetBranch}}
+ {{currentBranch}}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
index 29b76975561..353142edeb7 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -26,16 +26,6 @@ export default {
this.editMode = !this.editMode;
Store.toggleBlobView();
},
- toggleProjectRefsForm() {
- $('.project-refs-form').toggleClass('disabled', this.editMode);
- $('.js-tree-ref-target-holder').toggle(this.editMode);
- },
- },
-
- watch: {
- editMode() {
- this.toggleProjectRefsForm();
- },
},
};
</script>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 96d6a75bb61..02d9c775046 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -63,12 +63,7 @@ const RepoEditor = {
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
- Store.activeLine = lineNumber;
-
- Helper.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
+ Store.setActiveLine(lineNumber);
}
},
},
@@ -101,6 +96,15 @@ const RepoEditor = {
this.setupEditor();
}
},
+
+ activeLine() {
+ if (Helper.monacoInstance) {
+ Helper.monacoInstance.setPosition({
+ lineNumber: this.activeLine,
+ column: 1,
+ });
+ }
+ },
},
computed: {
shouldHideEditor() {
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 20ebf840774..8b9cbd23456 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -95,7 +95,7 @@ export default RepoFile;
</div>
</td>
- <td class="hidden-xs">
+ <td class="hidden-xs text-right">
<span
class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index 2200754cbef..a87bef6084a 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -1,29 +1,42 @@
<script>
+/* global LineHighlighter */
+
import Store from '../stores/repo_store';
export default {
data: () => Store,
- mounted() {
- this.highlightFile();
- },
computed: {
html() {
return this.activeFile.html;
},
},
-
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
+ highlightLine() {
+ if (Store.activeLine > -1) {
+ this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
+ }
+ },
+ },
+ mounted() {
+ this.highlightFile();
+ this.lineHighlighter = new LineHighlighter({
+ fileHolderSelector: '.blob-viewer-container',
+ scrollFileHolder: true,
+ });
},
-
watch: {
html() {
this.$nextTick(() => {
this.highlightFile();
+ this.highlightLine();
});
},
+ activeLine() {
+ this.highlightLine();
+ },
},
};
</script>
@@ -45,7 +58,7 @@ export default {
v-else
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 3414128526d..e0f3c33003a 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -18,36 +18,64 @@ export default {
},
created() {
- this.addPopEventListener();
+ window.addEventListener('popstate', this.checkHistory);
+ },
+ destroyed() {
+ window.removeEventListener('popstate', this.checkHistory);
},
data: () => Store,
methods: {
- addPopEventListener() {
- window.addEventListener('popstate', () => {
- if (location.href.indexOf('#') > -1) return;
- this.linkClicked({
+ checkHistory() {
+ let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
+ if (!selectedFile) {
+ // Maybe it is not in the current tree but in the opened tabs
+ selectedFile = Helper.getFileFromPath(location.pathname);
+ }
+
+ let lineNumber = null;
+ if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
+
+ if (selectedFile) {
+ if (selectedFile.url !== this.activeFile.url) {
+ this.fileClicked(selectedFile, lineNumber);
+ } else {
+ Store.setActiveLine(lineNumber);
+ }
+ } else {
+ // Not opened at all lets open new tab
+ this.fileClicked({
url: location.href,
- });
- });
+ }, lineNumber);
+ }
},
- fileClicked(clickedFile) {
+ fileClicked(clickedFile, lineNumber) {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
+
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
+ Store.setActiveLine(lineNumber);
} else {
- Service.url = file.url;
- Helper.getContent(file)
- .then(() => {
- file.loading = false;
- Helper.scrollTabsRight();
- })
- .catch(Helper.loadingError);
+ const openFile = Helper.getFileFromPath(file.url);
+ if (openFile) {
+ file.loading = false;
+ Store.setActiveFiles(openFile);
+ Store.setActiveLine(lineNumber);
+ } else {
+ Service.url = file.url;
+ Helper.getContent(file)
+ .then(() => {
+ file.loading = false;
+ Helper.scrollTabsRight();
+ Store.setActiveLine(lineNumber);
+ })
+ .catch(Helper.loadingError);
+ }
}
},
@@ -67,8 +95,8 @@ export default {
<thead v-if="!isMini">
<tr>
<th class="name">Name</th>
- <th class="hidden-sm hidden-xs last-commit">Last Commit</th>
- <th class="hidden-xs last-update">Last Update</th>
+ <th class="hidden-sm hidden-xs last-commit">Last commit</th>
+ <th class="hidden-xs last-update text-right">Last update</th>
</tr>
</thead>
<tbody>
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
index 2bd8d7eea65..7483f8bc305 100644
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -58,13 +58,13 @@ const RepoHelper = {
return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
},
- setDirectoryOpen(tree) {
+ setDirectoryOpen(tree, title) {
const file = tree;
if (!file) return undefined;
file.opened = true;
file.icon = 'fa-folder-open';
- RepoHelper.updateHistoryEntry(file.url, file.name);
+ RepoHelper.updateHistoryEntry(file.url, title);
return file;
},
@@ -135,6 +135,8 @@ const RepoHelper = {
return Service.getContent()
.then((response) => {
const data = response.data;
+ if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
+
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
@@ -168,7 +170,7 @@ const RepoHelper = {
} else {
// it's a tree
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
- file = RepoHelper.setDirectoryOpen(file);
+ file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.prevURL = Service.blobURLtoParentTree(Service.url);
@@ -178,8 +180,8 @@ const RepoHelper = {
setFile(data, file) {
const newFile = data;
+ newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
- newFile.url = file.url;
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
@@ -252,10 +254,12 @@ const RepoHelper = {
RepoHelper.key = RepoHelper.genKey();
- history.pushState({ key: RepoHelper.key }, '', url);
+ if (document.location.pathname !== url) {
+ history.pushState({ key: RepoHelper.key }, '', url);
+ }
if (title) {
- document.title = `${title} · GitLab`;
+ document.title = title;
}
},
@@ -263,6 +267,10 @@ const RepoHelper = {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
},
+ getFileFromPath(path) {
+ return Store.openedFiles.find(file => file.url === path);
+ },
+
loadingError() {
Flash('Unable to load this content at this time.');
},
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
index 6c1d468e937..7d0123e3d3a 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -11,10 +11,6 @@ function initDropdowns() {
}
function addEventsForNonVueEls() {
- $(document).on('change', '.dropdown', () => {
- Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
- });
-
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
index 1c0df528aea..93b39cff27e 100644
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -26,13 +26,12 @@ const RepoStore = {
},
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
- activeLine: 0,
+ activeLine: -1,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
binary: false,
currentBranch: '',
- targetBranch: 'new-branch',
commitMessage: '',
binaryTypes: {
png: false,
@@ -84,8 +83,9 @@ const RepoStore = {
}).catch(Helper.loadingError);
}
- if (!file.loading) Helper.updateHistoryEntry(file.url, file.name);
+ if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
RepoStore.binary = file.binary;
+ RepoStore.setActiveLine(-1);
},
setFileActivity(file, openedFile, i) {
@@ -102,6 +102,10 @@ const RepoStore = {
RepoStore.activeFileIndex = i;
},
+ setActiveLine(activeLine) {
+ if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
+ },
+
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index a4eae135403..a41548bd694 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -29,28 +29,32 @@ import Cookies from 'js-cookie';
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
- e.preventDefault();
- $this = $(this);
- $thisIcon = $this.find('i');
- $allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
- $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
- $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- }
- if (!triggered) {
- return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
- }
- });
+ $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
};
+ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+ var $allGutterToggleIcons, $this, $thisIcon;
+ e.preventDefault();
+ $this = $(this);
+ $thisIcon = $this.find('i');
+ $allGutterToggleIcons = $('.js-sidebar-toggle i');
+ if ($thisIcon.hasClass('fa-angle-double-right')) {
+ $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
+ $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
+ $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+
+ if (gl.lazyLoader) gl.lazyLoader.loadCheck();
+ }
+ if (!triggered) {
+ Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ }
+ };
+
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 003a15592f3..f15452ec683 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,4 +1,5 @@
/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
((global) => {
const KEYCODE = {
@@ -146,14 +147,14 @@
}
getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName;
userId = gon.current_user_id;
userName = gon.current_username;
- utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
- if (utils.isInGroupsPage() && groupOptions) {
- options = groupOptions[utils.getGroupSlug()];
- } else if (utils.isInProjectPage() && projectOptions) {
- options = projectOptions[utils.getProjectSlug()];
+ projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
+ if (isInGroupsPage() && groupOptions) {
+ options = groupOptions[getGroupSlug()];
+ } else if (isInProjectPage() && projectOptions) {
+ options = projectOptions[getProjectSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
}
@@ -286,6 +287,7 @@
onClearInputClick(e) {
e.preventDefault();
+ this.wrap.toggleClass('has-value', !!e.target.value);
return this.searchInput.val('').focus();
}
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e3daa8cf949..f63b99ba1c5 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
+import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
@@ -17,23 +18,8 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar);
- const $globalDropdownMenu = $('.global-dropdown-menu');
- const $globalDropdownToggle = $('.global-dropdown-toggle');
const findFileURL = document.body.dataset.findFile;
- $('.global-dropdown').on('hide.bs.dropdown', () => {
- $globalDropdownMenu.removeClass('shortcuts');
- });
-
- Mousetrap.bind('n', () => {
- $globalDropdownMenu.toggleClass('shortcuts');
- $globalDropdownToggle.trigger('click');
-
- if (!$globalDropdownMenu.is(':visible')) {
- $globalDropdownToggle.blur();
- }
- });
-
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8e7abdbffef..f2b1099a678 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -47,9 +47,9 @@ export default {
</script>
<template>
- <div class="block confidentiality">
+ <div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
+ <i class="fa" :class="faEye" aria-hidden="true"></i>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -62,19 +62,19 @@ export default {
Edit
</a>
</div>
- <div class="value confidential-value hide-collapsed">
+ <div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
- <div v-if="!isConfidential" class="no-value confidential-value">
- <i class="fa fa-eye is-not-confidential"></i>
+ <div v-if="!isConfidential" class="no-value sidebar-item-value">
+ <i class="fa fa-eye sidebar-item-icon"></i>
Not confidential
</div>
- <div v-else class="value confidential-value hide-collapsed">
- <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
+ <div v-else class="value sidebar-item-value hide-collapsed">
+ <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index d578b663a54..dd17b5abd46 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -2,9 +2,6 @@
import editFormButtons from './edit_form_buttons.vue';
export default {
- components: {
- editFormButtons,
- },
props: {
isConfidential: {
required: true,
@@ -19,12 +16,16 @@ export default {
type: Function,
},
},
+
+ components: {
+ editFormButtons,
+ },
};
</script>
<template>
<div class="dropdown open">
- <div class="dropdown-menu confidential-warning-message">
+ <div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 97af4a3f505..7ed0619ee6b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -15,7 +15,7 @@ export default {
},
},
computed: {
- onOrOff() {
+ toggleButtonText() {
return this.isConfidential ? 'Turn Off' : 'Turn On';
},
updateConfidentialBool() {
@@ -26,7 +26,7 @@ export default {
</script>
<template>
- <div class="confidential-warning-message-actions">
+ <div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@@ -39,7 +39,7 @@ export default {
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
>
- {{ onOrOff }}
+ {{ toggleButtonText }}
</button>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
new file mode 100644
index 00000000000..c7a6edc7c70
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -0,0 +1,61 @@
+<script>
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+
+ mixins: [
+ issuableMixin,
+ ],
+
+ components: {
+ editFormButtons,
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown open">
+ <div class="dropdown-menu sidebar-item-warning-message">
+ <p class="text" v-if="isLocked">
+ Unlock this {{ issuableDisplayName(issuableType) }}?
+ <strong>Everyone</strong>
+ will be able to comment.
+ </p>
+
+ <p class="text" v-else>
+ Lock this {{ issuableDisplayName(issuableType) }}?
+ Only
+ <strong>project members</strong>
+ will be able to comment.
+ </p>
+
+ <edit-form-buttons
+ :is-locked="isLocked"
+ :toggle-form="toggleForm"
+ :update-locked-attribute="updateLockedAttribute"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
new file mode 100644
index 00000000000..c3a553a7605
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -0,0 +1,50 @@
+<script>
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+
+ computed: {
+ buttonText() {
+ return this.isLocked ? this.__('Unlock') : this.__('Lock');
+ },
+
+ toggleLock() {
+ return !this.isLocked;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="sidebar-item-warning-message-actions">
+ <button
+ type="button"
+ class="btn btn-default append-right-10"
+ @click="toggleForm"
+ >
+ {{ __('Cancel') }}
+ </button>
+
+ <button
+ type="button"
+ class="btn btn-close"
+ @click.prevent="updateLockedAttribute(toggleLock)"
+ >
+ {{ buttonText }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
new file mode 100644
index 00000000000..c4b2900e020
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -0,0 +1,120 @@
+<script>
+/* global Flash */
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
+
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
+ },
+ },
+
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+
+ mixins: [
+ issuableMixin,
+ ],
+
+ components: {
+ editForm,
+ },
+
+ computed: {
+ lockIconClass() {
+ return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ },
+
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
+ },
+ },
+
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service.update(this.issuableType, {
+ discussion_locked: locked,
+ })
+ .then(() => location.reload())
+ .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block issuable-sidebar-item lock">
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa"
+ :class="lockIconClass"
+ aria-hidden="true"
+ ></i>
+ </div>
+
+ <div class="title hide-collapsed">
+ Lock {{issuableDisplayName(issuableType) }}
+ <button
+ v-if="isEditable"
+ class="pull-right lock-edit btn btn-blank"
+ type="button"
+ @click.prevent="toggleForm"
+ >
+ {{ __('Edit') }}
+ </button>
+ </div>
+
+ <div class="value sidebar-item-value hide-collapsed">
+ <edit-form
+ v-if="isLockDialogOpen"
+ :toggle-form="toggleForm"
+ :is-locked="isLocked"
+ :update-locked-attribute="updateLockedAttribute"
+ :issuable-type="issuableType"
+ />
+
+ <div
+ v-if="isLocked"
+ class="value sidebar-item-value"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-lock sidebar-item-icon is-active"
+ ></i>
+ {{ __('Locked') }}
+ </div>
+
+ <div
+ v-else
+ class="no-value sidebar-item-value hide-collapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-unlock sidebar-item-icon"
+ ></i>
+ {{ __('Unlocked') }}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
index 0da265053bd..a9fbc7f1a2f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
@@ -1,6 +1,5 @@
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
-
-import '../../../lib/utils/pretty_time';
+import { abbreviateTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-collapsed-state',
@@ -79,7 +78,7 @@ export default {
},
methods: {
abbreviateTime(timeStr) {
- return gl.utils.prettyTime.abbreviateTime(timeStr);
+ return abbreviateTime(timeStr);
},
},
template: `
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
index 40f5c89c5bb..fd0d4570d68 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
@@ -1,6 +1,4 @@
-import '../../../lib/utils/pretty_time';
-
-const prettyTime = gl.utils.prettyTime;
+import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-comparison-pane',
@@ -23,12 +21,12 @@ export default {
},
},
computed: {
- parsedRemaining() {
+ parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
- return prettyTime.parseSeconds(diffSeconds);
+ return parseSeconds(diffSeconds);
},
timeRemainingHumanReadable() {
- return prettyTime.stringifyTime(this.parsedRemaining);
+ return stringifyTime(this.parsedTimeRemaining);
},
timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
@@ -44,13 +42,6 @@ export default {
timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
- /* Parsed time values */
- parsedEstimate() {
- return prettyTime.parseSeconds(this.timeEstimate);
- },
- parsedSpent() {
- return prettyTime.parseSeconds(this.timeSpent);
- },
},
template: `
<div class="time-tracking-comparison-pane">
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 1c15a1b877a..3c9de02407e 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -38,7 +38,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
- .catch(() => new Flash('An error occured while fetching projects autocomplete.'));
+ .catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
@@ -73,7 +73,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
- Flash('An error occured while moving the issue.');
+ Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 3d8972050a9..09b9d75c02d 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,46 +1,76 @@
import Vue from 'vue';
-import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
-import sidebarAssignees from './components/assignees/sidebar_assignees';
-import confidential from './components/confidential/confidential_issue_sidebar.vue';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarAssignees from './components/assignees/sidebar_assignees';
+import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
+import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
+Vue.use(Translate);
+
+function mountConfidentialComponent(mediator) {
+ const el = document.getElementById('js-confidential-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-confidential-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
+
+ new ConfidentialComp({
+ propsData: {
+ isConfidential: initialData.is_confidential,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }).$mount(el);
+}
+
+function mountLockComponent(mediator) {
+ const el = document.getElementById('js-lock-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-lock-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const LockComp = Vue.extend(LockIssueSidebar);
+
+ new LockComp({
+ propsData: {
+ isLocked: initialData.is_locked,
+ isEditable: initialData.is_editable,
+ mediator,
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
+ },
+ }).$mount(el);
+}
+
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
- const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
- const confidentialEl = document.querySelector('#js-confidential-entry-point');
+ const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
- new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
+ new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
- if (confidentialEl) {
- const dataNode = document.getElementById('js-confidential-issue-data');
- const initialData = JSON.parse(dataNode.innerHTML);
+ mountConfidentialComponent(mediator);
+ mountLockComponent(mediator);
- const ConfidentialComp = Vue.extend(confidential);
-
- new ConfidentialComp({
- propsData: {
- isConfidential: initialData.is_confidential,
- isEditable: initialData.is_editable,
- service: mediator.service,
- },
- }).$mount(confidentialEl);
-
- new SidebarMoveIssue(
- mediator,
- $('.js-move-issue'),
- $('.js-move-issue-confirmation-button'),
- ).init();
- }
+ new SidebarMoveIssue(
+ mediator,
+ $('.js-move-issue'),
+ $('.js-move-issue-confirmation-button'),
+ ).init();
- new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
+ new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index e38a8db4cc5..2fe6e5b31f0 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -41,7 +41,7 @@ export default class SidebarMediator {
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
})
- .catch(() => new Flash('Error occured when fetching sidebar data'));
+ .catch(() => new Flash('Error occurred when fetching sidebar data'));
}
fetchAutocompleteProjects(searchTerm) {
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index cc04a2a3fcf..d5d04103f3f 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -15,6 +15,7 @@ export default class SidebarStore {
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
+ this.isLockDialogOpen = false;
SidebarStore.singleton = this;
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 4505a79a2df..3f811c59cb9 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
+import imageDiffHelper from './image_diff/helpers/index';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl.diffNotesCompileComponents();
}
- FilesCommentButton.init($(_this.file));
+ const $file = $(_this.file);
+ FilesCommentButton.init($file);
+
+ const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
+ imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb();
};
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index a606852c22c..2fffe09c74e 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
import UsersSelect from './users_select';
+import { isMetaClick } from './lib/utils/common_utils';
export default class Todos {
constructor() {
@@ -137,22 +138,17 @@ export default class Todos {
goToTodoUrl(e) {
const todoLink = this.dataset.url;
- if (!todoLink) {
+ if (!todoLink || e.target.tagName === 'A' || e.target.tagName === 'IMG') {
return;
}
- if (gl.utils.isMetaClick(e)) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (isMetaClick(e)) {
const windowTarget = '_blank';
- const selected = e.target;
- e.stopPropagation();
- e.preventDefault();
-
- if (selected.tagName === 'IMG') {
- const avatarUrl = selected.parentElement.getAttribute('href');
- window.open(avatarUrl, windowTarget);
- } else {
- window.open(todoLink, windowTarget);
- }
+
+ window.open(todoLink, windowTarget);
} else {
gl.utils.visitUrl(todoLink);
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index a31fedee021..73676bd6de7 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -75,7 +75,7 @@ function UsersSelect(currentUser, els) {
if (currentUserInfo) {
input.value = currentUserInfo.id;
- input.dataset.meta = currentUserInfo.name;
+ input.dataset.meta = _.escape(currentUserInfo.name);
} else if (_this.currentUser) {
input.value = _this.currentUser.id;
}
@@ -198,7 +198,7 @@ function UsersSelect(currentUser, els) {
};
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', user.name).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
@@ -506,7 +506,7 @@ function UsersSelect(currentUser, els) {
img = "";
if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`;
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else {
if (avatar) {
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
@@ -518,7 +518,7 @@ function UsersSelect(currentUser, els) {
<a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
${img}
<strong class='dropdown-menu-user-full-name'>
- ${user.name}
+ ${_.escape(user.name)}
</strong>
${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
</a>
@@ -643,11 +643,11 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>";
+ return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + _.escape(user.name) + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>";
};
UsersSelect.prototype.formatSelection = function(user) {
- return user.name;
+ return _.escape(user.name);
};
UsersSelect.prototype.user = function(user_id, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index aaca42e3ebc..219ff94924e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -72,12 +72,12 @@ export default {
<a
href="#modal_merge_info"
data-toggle="modal"
- class="btn btn-small inline">
+ class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
- class="btn btn-small inline dropdown-toggle"
+ class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index a4e34116c33..a8c686e5065 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -1,6 +1,6 @@
import statusCodes from '../../lib/utils/http_status';
import { bytesToMiB } from '../../lib/utils/number_utils';
-
+import { backOff } from '../../lib/utils/common_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
@@ -84,7 +84,7 @@ export default {
}
},
loadMetrics() {
- gl.utils.backOff((next, stop) => {
+ backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index 6c2e9ba1d30..c79b5c720eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -12,6 +12,9 @@ export default {
ciIcon,
},
computed: {
+ hasPipeline() {
+ return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
+ },
hasCIError() {
const { hasCI, ciStatus } = this.mr;
@@ -28,7 +31,9 @@ export default {
},
},
template: `
- <div class="mr-widget-heading">
+ <div
+ v-if="hasPipeline || hasCIError"
+ class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
@@ -40,7 +45,7 @@ export default {
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
- <template v-else>
+ <template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10">
<a
class="icon-link"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
index b01c923311b..4998a47b691 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -27,7 +27,7 @@ export default {
<button
v-if="showDisabledButton"
type="button"
- class="btn btn-success btn-small"
+ class="js-disabled-merge-button btn btn-success btn-sm"
disabled="true">
Merge
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
index 2b16a2d6817..b4e4a6aa161 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -11,7 +11,7 @@ export default {
<status-icon status="failed" />
<button
type="button"
- class="btn btn-success btn-small"
+ class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
index aaf9d3304a4..09561694939 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="loading" showDisabledButton />
+ <status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Checking ability to merge automatically
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index 4078aad7f83..b25cc3443ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -16,9 +16,9 @@ export default {
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
- :author="mr.closedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.closedAt"
+ :author="mr.closedEvent.author"
+ :dateTitle="mr.closedEvent.updatedAt"
+ :dateReadable="mr.closedEvent.formattedUpdatedAt"
/>
<section class="mr-info-list">
<p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index f9cb79a0bc1..5d468a085cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -10,27 +10,37 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon
+ status="failed"
+ :show-disabled-button="true" />
<div class="media-body space-children">
- <span class="bold">
- There are merge conflicts<span v-if="!mr.canMerge">.</span>
- <span v-if="!mr.canMerge">
- Resolve these conflicts or ask someone with write access to this repository to merge it locally
- </span>
+ <span
+ v-if="mr.shouldBeRebased"
+ class="bold">
+ Fast-forward merge is not possible.
+ To merge this request, first rebase locally.
</span>
- <a
- v-if="mr.canMerge && mr.conflictResolutionPath"
- :href="mr.conflictResolutionPath"
- class="btn btn-default btn-xs js-resolve-conflicts-button">
- Resolve conflicts
- </a>
- <a
- v-if="mr.canMerge"
- class="btn btn-default btn-xs js-merge-locally-button"
- data-toggle="modal"
- href="#modal_merge_info">
- Merge locally
- </a>
+ <template v-else>
+ <span class="bold">
+ There are merge conflicts<span v-if="!mr.canMerge">.</span>
+ <span v-if="!mr.canMerge">
+ Resolve these conflicts or ask someone with write access to this repository to merge it locally
+ </span>
+ </span>
+ <a
+ v-if="mr.canMerge && mr.conflictResolutionPath"
+ :href="mr.conflictResolutionPath"
+ class="js-resolve-conflicts-button btn btn-default btn-xs">
+ Resolve conflicts
+ </a>
+ <a
+ v-if="mr.canMerge"
+ class="js-merge-locally-button btn btn-default btn-xs"
+ data-toggle="modal"
+ href="#modal_merge_info">
+ Merge locally
+ </a>
+ </template>
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index 1cb24549d53..c25d6c359bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index e452260a4d0..74fc52796a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -69,9 +69,9 @@ export default {
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
- :author="mr.mergedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.mergedAt" />
+ :author="mr.mergedEvent.author"
+ :date-title="mr.mergedEvent.updatedAt"
+ :date-readable="mr.mergedEvent.formattedUpdatedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 9f0a359d01a..1bc0b7e0819 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 797511d4e3a..00047718201 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" showDisabledButton />
+ <status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 167a0d4613a..1cedf86e811 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index c5be9a0530a..6853ba4b9f8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 65187754009..61734163b6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -29,34 +29,53 @@ export default {
statusIcon,
},
computed: {
+ shouldShowMergeWhenPipelineSucceedsText() {
+ return this.mr.isPipelineActive;
+ },
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
- mergeButtonClass() {
- const defaultClass = 'btn btn-small btn-success accept-merge-request';
- const failedClass = `${defaultClass} btn-danger`;
- const inActionClass = `${defaultClass} btn-info`;
+ status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
- return failedClass;
+ return 'failed';
} else if (!pipeline) {
- return defaultClass;
+ return 'success';
} else if (isPipelineActive) {
- return inActionClass;
+ return 'pending';
} else if (isPipelineFailed) {
+ return 'failed';
+ }
+
+ return 'success';
+ },
+ mergeButtonClass() {
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+
+ if (this.status === 'failed') {
return failedClass;
+ } else if (this.status === 'pending') {
+ return inActionClass;
}
return defaultClass;
},
+ iconClass() {
+ if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
+ return 'failed';
+ }
+ return 'success';
+ },
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
- } else if (this.mr.isPipelineActive) {
+ } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
return 'Merge when pipeline succeeds';
}
@@ -68,7 +87,7 @@ export default {
isMergeButtonDisabled() {
const { commitMessage } = this;
return Boolean(!commitMessage.length
- || !this.isMergeAllowed()
+ || !this.shouldShowMergeControls()
|| this.isMakingRequest
|| this.mr.preventMerge);
},
@@ -81,8 +100,8 @@ export default {
},
},
methods: {
- isMergeAllowed() {
- return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
+ shouldShowMergeControls() {
+ return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
@@ -148,6 +167,7 @@ export default {
eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
+ window.mergeRequest.hideCloseButton();
window.mergeRequest.decreaseCounter();
}
stopPolling();
@@ -200,10 +220,10 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <status-icon :status="iconClass" />
<div class="media-body">
- <div class="media space-children">
- <span class="btn-group">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group append-bottom-5">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
@@ -219,7 +239,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
- class="btn btn-small btn-info dropdown-toggle js-merge-moment"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
aria-label="Select merge moment">
<i
@@ -260,8 +280,8 @@ export default {
</li>
</ul>
</span>
- <div class="media-body space-children">
- <template v-if="isMergeAllowed()">
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls()">
<label>
<input
id="remove-source-branch-input"
@@ -276,17 +296,23 @@ export default {
:mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" />
+ <span
+ v-if="mr.ffOnlyEnabled"
+ class="js-fast-forward-message">
+ Fast-forward merge without a merge commit
+ </span>
<button
+ v-else
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- class="btn btn-default btn-xs"
+ class="js-modify-commit-message-button btn btn-default btn-xs"
type="button">
Modify commit message
</button>
</template>
<template v-else>
<span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ The pipeline for this merge request has not succeeded yet
</span>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 89f38e5bd2a..af19cf6ab87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index d762ca6e640..a119ecbbdfe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index b11a06899cf..54be1fbe675 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -38,7 +38,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
+ <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 0042c48816f..044b664484b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -31,6 +31,7 @@ import {
SquashBeforeMerge,
notify,
} from './dependencies';
+import { setFavicon } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
@@ -57,7 +58,7 @@ export default {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
},
shouldRenderPipelines() {
- return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
+ return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
return this.mr.relatedLinks;
@@ -86,7 +87,7 @@ export default {
.then((res) => {
this.handleNotification(res);
this.mr.setData(res);
- this.setFavicon();
+ this.setFaviconHelper();
if (cb) {
cb.call(null, res);
@@ -115,9 +116,9 @@ export default {
immediateExecution: true,
});
},
- setFavicon() {
+ setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
- gl.utils.setFavicon(this.mr.ciStatusFaviconPath);
+ setFavicon(this.mr.ciStatusFaviconPath);
}
},
fetchDeployments() {
@@ -193,7 +194,7 @@ export default {
});
},
handleMounted() {
- this.setFavicon();
+ this.setFaviconHelper();
this.initDeploymentsPolling();
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index fbea764b739..c1f7e64f580 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -37,10 +37,8 @@ export default class MergeRequestStore {
}
this.updatedAt = data.updated_at;
- this.mergedAt = MergeRequestStore.getEventDate(data.merge_event);
- this.closedAt = MergeRequestStore.getEventDate(data.closed_event);
- this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
- this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
+ this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
+ this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
@@ -57,6 +55,8 @@ export default class MergeRequestStore {
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
+ this.ffOnlyEnabled = data.ff_only_enabled;
+ this.shouldBeRebased = !!data.should_be_rebased;
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
+ this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related
@@ -85,7 +86,9 @@ export default class MergeRequestStore {
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
- this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
+ this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
@@ -116,6 +119,14 @@ export default class MergeRequestStore {
}
}
+ static getEventObject(event) {
+ return {
+ author: MergeRequestStore.getAuthorObject(event),
+ updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
+ formattedUpdatedAt: MergeRequestStore.getEventDate(event),
+ };
+ }
+
static getAuthorObject(event) {
if (!event) {
return {};
@@ -129,6 +140,14 @@ export default class MergeRequestStore {
};
}
+ static getEventUpdatedAtDate(event) {
+ if (!event) {
+ return '';
+ }
+
+ return event.updated_at;
+ }
+
static getEventDate(event) {
const timeagoInstance = new Timeago();
@@ -136,7 +155,7 @@ export default class MergeRequestStore {
return '';
}
- return timeagoInstance.format(event.updated_at);
+ return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
}
}
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
new file mode 100644
index 00000000000..3a7143c450e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -0,0 +1,32 @@
+<script>
+ /**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ */
+
+ export default {
+ name: 'clipboardButton',
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn btn-transparent btn-clipboard"
+ :data-title="title"
+ :data-clipboard-text="text">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue
deleted file mode 100644
index 397d16331d5..00000000000
--- a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<script>
- export default {
- name: 'confidentialIssueWarning',
- };
-</script>
-<template>
- <div class="confidential-issue-warning">
- <i
- aria-hidden="true"
- class="fa fa-eye-slash">
- </i>
- <span>
- This is a confidential issue. Your comment will not be visible to the public.
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
new file mode 100644
index 00000000000..16c0a8efcd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -0,0 +1,55 @@
+<script>
+ export default {
+ props: {
+ isLocked: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+
+ isConfidential: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+
+ computed: {
+ iconClass() {
+ return {
+ 'fa-eye-slash': this.isConfidential,
+ 'fa-lock': this.isLocked,
+ };
+ },
+
+ isLockedAndConfidential() {
+ return this.isConfidential && this.isLocked;
+ },
+ },
+ };
+</script>
+<template>
+ <div class="issuable-note-warning">
+ <i
+ aria-hidden="true"
+ class="fa icon"
+ :class="iconClass"
+ v-if="!isLockedAndConfidential"
+ ></i>
+
+ <span v-if="isLockedAndConfidential">
+ {{ __('This issue is confidential and locked.') }}
+ {{ __('People without permission will never get a notification and won\'t be able to comment.') }}
+ </span>
+
+ <span v-else-if="isConfidential">
+ {{ __('This is a confidential issue.') }}
+ {{ __('Your comment will not be visible to the public.') }}
+ </span>
+
+ <span v-else-if="isLocked">
+ {{ __('This issue is locked.') }}
+ {{ __('Only project members can comment.') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 994b33bc1c9..9279b50cd55 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -7,7 +7,7 @@ export default {
type: String,
required: true,
},
- body: {
+ text: {
type: String,
required: true,
},
@@ -63,7 +63,9 @@ export default {
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
- <p>{{this.body}}</p>
+ <slot name="body" :text="text">
+ <p>{{text}}</p>
+ </slot>
</div>
<div class="modal-footer">
<button
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index c9dbc048345..710452bb3d3 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -1,11 +1,13 @@
<script>
+import { s__ } from '../../locale';
+
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
-const PREV = 'Prev';
-const NEXT = 'Next';
-const FIRST = '« First';
-const LAST = 'Last »';
+const PREV = s__('Pagination|Prev');
+const NEXT = s__('Pagination|Next');
+const FIRST = s__('Pagination|« First');
+const LAST = s__('Pagination|Last »');
export default {
props: {
diff --git a/app/assets/javascripts/vue_shared/mixins/issuable.js b/app/assets/javascripts/vue_shared/mixins/issuable.js
new file mode 100644
index 00000000000..263361587e0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/issuable.js
@@ -0,0 +1,9 @@
+export default {
+ methods: {
+ issuableDisplayName(issuableType) {
+ const displayName = issuableType.replace(/_/, ' ');
+
+ return this.__ ? this.__(displayName) : displayName;
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js
index f83c4b00761..2c7886ec308 100644
--- a/app/assets/javascripts/vue_shared/translate.js
+++ b/app/assets/javascripts/vue_shared/translate.js
@@ -2,6 +2,7 @@ import {
__,
n__,
s__,
+ sprintf,
} from '../locale';
export default (Vue) => {
@@ -37,6 +38,7 @@ export default (Vue) => {
@returns {String} Translated context based text
**/
s__,
+ sprintf,
},
});
};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 7f8e514fda1..b9693892f45 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
+import csrf from '../lib/utils/csrf';
Vue.use(VueResource);
@@ -18,9 +19,7 @@ Vue.http.interceptors.push((request, next) => {
// New Vue Resource version uses Headers, we are expecting a plain object to render pagination
// and polling.
Vue.http.interceptors.push((request, next) => {
- if ($.rails) {
- request.headers.set('X-CSRF-Token', $.rails.csrfToken());
- }
+ request.headers.set(csrf.headerKey, csrf.token);
next((response) => {
// Headers object has a `forEach` property that iterates through all values.
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 35e7a10379f..c7be94e2c8e 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -30,14 +30,16 @@
@import "framework/media_object";
@import "framework/mobile";
@import "framework/modal";
-@import "framework/nav";
@import "framework/pagination";
@import "framework/panels";
+@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
+@import "framework/new-sidebar";
@import "framework/tables";
@import "framework/notes";
@import "framework/timeline";
+@import "framework/tooltips";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
@@ -52,4 +54,3 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
-@import "framework/feature_highlight";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 667b73e150d..f0e6b23757f 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -115,8 +115,7 @@
@return $unfoldedTransition;
}
-.btn,
-.global-dropdown-toggle {
+.btn {
@include transition(background-color, border-color, color, box-shadow);
}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index bdcbd4021b3..f1aedc227f3 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
+ &.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); }
@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
+ &.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index bb30da4f4b2..e0d2ed80de5 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -9,6 +9,7 @@
}
.emoji-menu {
+ display: none;
position: absolute;
top: 0;
margin-top: 3px;
@@ -27,6 +28,10 @@
transition: .3s cubic-bezier(.67, .06, .19, 1.44);
transition-property: transform, opacity;
+ &.is-rendered {
+ display: block;
+ }
+
&.is-aligned-right {
transform-origin: 100% -45px;
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index b575ec9de18..dbd990f84c1 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -207,6 +207,16 @@
&.user-cover-block {
padding: 24px 0 0;
+
+ .nav-links {
+ justify-content: center;
+ width: 100%;
+ float: none;
+
+ &.scrolling-tabs {
+ float: none;
+ }
+ }
}
.group-info {
@@ -260,7 +270,7 @@
position: relative;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
- background-color: $blue-25;
+ background-color: $blue-50;
justify-content: center;
.dismiss-button {
@@ -319,16 +329,6 @@
padding: $gl-padding;
}
- .svg-content {
- text-align: center;
-
- svg {
- max-width: 425px;
- width: 100%;
- padding: $gl-padding;
- }
- }
-
.emoji-icon {
display: inline-block;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 82350c36df0..b131e2d57ee 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,3 +1,25 @@
+@mixin btn-comment-icon {
+ border-radius: 50%;
+ background: $white-light;
+ padding: 1px 5px;
+ font-size: 12px;
+ color: $blue-500;
+ width: 23px;
+ height: 23px;
+ border: 1px solid $blue-500;
+
+ &:hover,
+ &.inverted {
+ background: $blue-500;
+ border-color: $blue-600;
+ color: $white-light;
+ }
+
+ &:active {
+ outline: 0;
+ }
+}
+
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
@@ -46,15 +68,6 @@
}
}
-@mixin btn-svg {
- svg {
- height: 15px;
- width: 15px;
- position: relative;
- top: 2px;
- }
-}
-
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light;
border-color: $border-light;
@@ -132,7 +145,6 @@
.btn {
@include btn-default;
@include btn-white;
- @include btn-svg;
color: $gl-text-color;
@@ -140,7 +152,6 @@
outline: 0;
}
- &.btn-small,
&.btn-sm {
padding: 4px 10px;
font-size: 13px;
@@ -232,6 +243,13 @@
}
}
+ svg {
+ height: 15px;
+ width: 15px;
+ position: relative;
+ top: 2px;
+ }
+
svg,
.fa {
&:not(:last-child) {
@@ -385,7 +403,11 @@
background: transparent;
border: 0;
+ &:hover,
+ &:active,
&:focus {
outline: 0;
+ background: transparent;
+ box-shadow: none;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 706a9cffe87..96f9dda26c4 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
+.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
@@ -129,11 +130,6 @@ span.update-author {
}
}
-.user-mention {
- color: $user-mention-color;
- font-weight: $gl-font-weight-bold;
-}
-
.field_with_errors {
display: inline;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 2bcd23a15e6..5b950ae0ba0 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -745,7 +745,11 @@
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
- li {
+ &.dropdown-open-top {
+ margin-bottom: $dropdown-vertical-offset;
+ }
+
+ li:not(.dropdown-bold-header) {
display: block;
padding: 0 1px;
@@ -779,6 +783,14 @@
white-space: normal;
width: 100%;
+ &.dropdown-menu-user-link {
+ white-space: nowrap;
+
+ .dropdown-menu-user-username {
+ display: block;
+ }
+ }
+
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
@@ -865,12 +877,19 @@
min-width: 100%;
}
}
+
+ header.navbar-gitlab-new .header-content .dropdown {
+ .dropdown-menu {
+ left: 0;
+ min-width: 100%;
+ }
+ }
}
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
-header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
+header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 2d6bc17d4ff..527e7d57c5c 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,4 +1,5 @@
gl-emoji {
+ font-style: normal;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
deleted file mode 100644
index ebae473df50..00000000000
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ /dev/null
@@ -1,94 +0,0 @@
-.feature-highlight {
- position: relative;
- margin-left: $gl-padding;
- width: 20px;
- height: 20px;
- cursor: pointer;
-
- &::before {
- content: '';
- display: block;
- position: absolute;
- top: 6px;
- left: 6px;
- width: 8px;
- height: 8px;
- background-color: $blue-500;
- border-radius: 50%;
- box-shadow: 0 0 0 rgba($blue-500, 0.4);
- animation: pulse-highlight 2s infinite;
- }
-
- &:hover::before,
- &.disable-animation::before {
- animation: none;
- }
-
- &[disabled]::before {
- display: none;
- }
-}
-
-.is-showing-fly-out {
- .feature-highlight {
- display: none;
- }
-}
-
-.feature-highlight-popover-content {
- display: none;
-
- hr {
- margin: $gl-padding * 0.5 0;
- }
-
- .btn-link {
- @include btn-svg;
-
- svg path {
- fill: currentColor;
- }
- }
-
- .dismiss-feature-highlight {
- padding: 0;
- }
-
- svg:first-child {
- width: 100%;
- background-color: $indigo-50;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- border-bottom: 1px solid darken($gray-normal, 8%);
- }
-}
-
-.popover .feature-highlight-popover-content {
- display: block;
-}
-
-.feature-highlight-popover {
- padding: 0;
-
- .popover-content {
- padding: 0;
- }
-}
-
-.feature-highlight-popover-sub-content {
- padding: 9px 14px;
-}
-
-@include keyframes(pulse-highlight) {
- 0% {
- box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
- }
-
- 70% {
- box-shadow: 0 0 0 10px transparent;
- }
-
- 100% {
- box-shadow: 0 0 0 0 transparent;
- }
-}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 8ad082f7a65..588ec1ff3bc 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -17,8 +17,11 @@
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
- padding-top: 64px;
- padding-bottom: 64px;
+
+ @media (min-width: $screen-md-min) {
+ padding-top: 64px;
+ padding-bottom: 64px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index dbdd5a4464b..34a35734acc 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -6,3 +6,14 @@
.gfm-commit_range {
@extend .commit-sha;
}
+
+.gfm-project_member {
+ padding: 0 2px;
+ border-radius: #{$border-radius-default / 2};
+ background-color: $user-mention-bg;
+
+ &:hover {
+ background-color: $user-mention-bg-hover;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 71f764923ff..52b87de7a3d 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -5,8 +5,8 @@
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
- header.navbar-gitlab-new {
- background: linear-gradient(to right, $color-900, $color-800);
+ .navbar-gitlab {
+ background-color: $color-900;
.navbar-collapse {
color: $color-200;
@@ -95,7 +95,7 @@
}
}
- .title {
+ .navbar .title {
> a {
&:hover,
&:focus {
@@ -126,7 +126,7 @@
.search-input-wrap {
.search-icon,
.clear-icon {
- color: rgba($color-200, .8);
+ fill: rgba($color-200, .8);
}
}
@@ -141,7 +141,7 @@
.search-input-wrap {
.search-icon {
- color: rgba($color-200, .8);
+ fill: rgba($color-200, .8);
}
}
}
@@ -158,11 +158,23 @@
box-shadow: inset 4px 0 0 $color-700;
> a {
- color: $color-900;
+ color: $color-800;
}
svg {
- fill: $color-900;
+ fill: $color-800;
+ }
+ }
+
+ .sidebar-top-level-items > li.active .badge {
+ color: $color-800;
+ }
+
+ .nav-links li.active a {
+ border-bottom-color: $color-500;
+
+ .badge {
+ font-weight: $gl-font-weight-bold;
}
}
}
@@ -188,9 +200,9 @@ body {
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
- header.navbar-gitlab-new {
- background: $theme-gray-100;
- box-shadow: 0 2px 0 0 $border-color;
+ .navbar-gitlab {
+ background-color: $theme-gray-100;
+ box-shadow: 0 1px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
@@ -230,17 +242,21 @@ body {
&:hover {
background-color: $white-light;
- box-shadow: inset 0 0 0 1px $blue-100;
+ box-shadow: inset 0 0 0 1px $blue-200;
.location-badge {
- box-shadow: inset 0 0 0 1px $blue-100;
+ box-shadow: inset 0 0 0 1px $blue-200;
}
}
}
.search-input-wrap {
.search-icon {
- color: $theme-gray-200;
+ fill: $theme-gray-200;
+ }
+
+ .search-input {
+ color: $gl-text-color;
}
}
@@ -261,5 +277,9 @@ body {
fill: $theme-gray-900;
}
}
+
+ .sidebar-top-level-items > li.active .badge {
+ color: $theme-gray-900;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index ab3c34df1fb..22945e935ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,195 +1,155 @@
-/*
- * Application Header
- *
- */
+.content-wrapper.page-with-new-nav {
+ margin-top: $header-height;
+}
-header {
+.navbar-gitlab {
@include new-style-dropdown;
- transition: padding $sidebar-transition-duration;
-
- &.navbar-empty {
- height: $header-height;
- background: $white-light;
- border-bottom: 1px solid $white-normal;
-
- .center-logo {
- margin: 8px 0;
- text-align: center;
-
- .tanuki-logo,
- img {
- height: 36px;
- }
- }
- }
-
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
- background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
- color: $gl-text-color-secondary;
border-radius: 0;
- @media (max-width: $screen-xs-min) {
- padding: 0 16px;
- }
-
- &.with-horizontal-nav {
- border-bottom: 0;
+ .logo-text {
+ line-height: initial;
- .navbar-border {
- height: 1px;
- position: absolute;
- right: 0;
- left: 0;
- bottom: -1px;
- background-color: $border-color;
- opacity: 0;
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
}
}
.container-fluid {
- width: 100% !important;
- filter: none;
padding: 0;
- .nav > li > a {
- color: currentColor;
- font-size: 18px;
- padding: 0;
- margin: (($header-height - 28) / 2) 3px;
- margin-left: 8px;
- height: 28px;
- min-width: 32px;
- line-height: 28px;
- text-align: center;
-
- &.header-user-dropdown-toggle {
- margin-left: 14px;
-
- &:hover,
- &:focus,
- &:active {
- .header-user-avatar {
- border-color: rgba($avatar-border, .2);
- }
- }
- }
-
- &:hover,
- &:focus,
- &:active {
- background-color: transparent;
- color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-
- .fa-caret-down {
- font-size: 14px;
- }
-
- .fa-chevron-down {
- position: relative;
- top: -3px;
- font-size: 10px;
- }
- }
-
.user-counter {
svg {
- height: 16px;
- width: 23px;
+ margin-right: 3px;
}
}
.navbar-toggle {
- color: $nav-toggle-gray;
- margin: 5px 0;
- border-radius: 0;
right: -10px;
- padding: 6px 10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin-right: -7px;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
- &:hover {
- background-color: $white-normal;
+ &:hover,
+ &:focus,
+ &.active {
+ color: currentColor;
+ background-color: transparent;
}
- &.active {
- color: $gl-text-color-secondary;
+ .more-icon,
+ .close-icon {
+ fill: $white-light;
+ margin: auto;
}
}
}
}
- &.navbar-gitlab-new {
- .fa-times {
+ .close-icon {
+ display: none;
+ }
+
+ .menu-expanded {
+ .more-icon {
display: none;
}
- .menu-expanded {
- .fa-ellipsis-v {
- display: none;
- }
-
- .fa-times {
- display: block;
- }
+ .close-icon {
+ display: block;
}
}
- .global-dropdown {
- position: absolute;
- left: -10px;
+ .header-content {
+ display: -webkit-flex;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ min-height: $header-height;
+ padding-left: 0;
- .badge {
- font-size: 11px;
+ .title-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: stretch;
+ align-items: stretch;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
+ padding-top: 0;
+ overflow: visible;
}
- li {
- &.active a {
- font-weight: $gl-font-weight-bold;
+ .title {
+ padding-right: 0;
+ color: currentColor;
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ margin: 0;
+ font-size: 18px;
+ vertical-align: top;
+ white-space: nowrap;
+
+ img {
+ height: 28px;
+ margin-right: 8px;
+ }
+
+ &.wrap {
+ white-space: normal;
}
- }
- }
- .global-dropdown-toggle {
- margin: 7px 0;
- font-size: 18px;
- padding: 6px 10px;
- border: none;
- background-color: $gray-light;
+ &.initializing {
+ opacity: 0;
+ }
- &:hover {
- background-color: $white-normal;
- }
+ a {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: $border-radius-default;
- &:focus {
- outline: none;
- background-color: $white-normal;
+ svg {
+ @media (min-width: $screen-sm-min) {
+ margin-right: 8px;
+ }
+ }
+ }
+
+ .project-item-select {
+ right: auto;
+ left: 0;
+ }
}
- }
- .header-content {
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: $header-height;
- padding-left: 30px;
+ .dropdown.open {
+ > a {
+ border-bottom-color: $white-light;
+ }
+ }
&.menu-expanded {
@media (max-width: $screen-xs-max) {
- .header-logo,
.title-container {
display: none;
}
@@ -199,111 +159,180 @@ header {
}
}
}
+ }
- .dropdown-menu {
- margin-top: -5px;
- }
+ .dropdown-bold-header {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ }
- .header-logo {
- display: inline-block;
- margin: 0 12px 0 2px;
- position: relative;
- top: 10px;
- transition-duration: .3s;
+ .navbar-collapse {
+ flex: 0 0 auto;
+ border-top: none;
+ padding: 0;
- svg,
- img {
- height: 28px;
- }
+ @media (max-width: $screen-xs-max) {
+ flex: 1 1 auto;
+ }
- &:hover {
- cursor: pointer;
+ .nav {
+ > li:not(.hidden-xs) a {
+ @media (max-width: $screen-xs-max) {
+ margin-left: 0;
+ min-width: 100%;
+ }
}
}
+ }
- .group-name-toggle {
- margin: 3px 5px;
- }
+ .container-fluid {
- .group-title {
- &.is-hidden {
- .hidable:not(:last-of-type) {
- display: none;
+ .navbar-nav {
+ @media (max-width: $screen-xs-max) {
+ display: -webkit-flex;
+ display: flex;
+ padding-right: 10px;
+ }
+
+ li {
+ .badge {
+ box-shadow: none;
+ font-weight: $gl-font-weight-bold;
}
}
}
- .title-container {
- display: flex;
- align-items: flex-start;
- flex: 1 1 auto;
- padding-top: 14px;
- overflow: hidden;
- }
+ .nav > li {
+ &.header-user {
+ @media (max-width: $screen-xs-max) {
+ padding-left: 10px;
+ }
+ }
- .title {
- position: relative;
- padding-right: 20px;
- margin: 0;
- font-size: 18px;
- line-height: 22px;
- display: inline-block;
- font-weight: $gl-font-weight-normal;
- color: $gl-text-color;
- vertical-align: top;
- white-space: nowrap;
+ > a {
+ will-change: color;
+ margin: 4px 2px;
+ padding: 6px 8px;
+ height: 32px;
- &.wrap {
- white-space: normal;
+ @media (max-width: $screen-xs-max) {
+ padding: 0;
+ }
+
+ &.header-user-dropdown-toggle {
+ margin-left: 2px;
+
+ .header-user-avatar {
+ margin-right: 0;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
+ }
+ }
+ }
}
- &.initializing {
- opacity: 0;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
}
- a {
- color: currentColor;
+ .impersonated-user,
+ .impersonated-user:hover {
+ margin-right: 1px;
+ background-color: $white-light;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
- &:hover {
- text-decoration: underline;
- color: $gl-header-nav-hover-color;
+ i {
+ color: $orange-500;
+ font-size: 20px;
}
}
- .dropdown-toggle-caret {
- color: $gl-text-color;
- border: transparent;
- background: transparent;
- position: absolute;
- top: 2px;
- right: 3px;
- width: 12px;
- line-height: 19px;
- padding: 0;
- font-size: 10px;
- text-align: center;
- cursor: pointer;
+ &.active > a,
+ &.dropdown.open > a {
- &:hover {
- color: $gl-header-nav-hover-color;
+ svg {
+ fill: currentColor;
}
}
+ }
+ }
+}
- .project-item-select {
- right: auto;
- left: 0;
+.navbar-sub-nav,
+.navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+
+ svg {
+ fill: currentColor;
}
}
- .navbar-collapse {
- flex: 0 0 auto;
- border-top: none;
- padding: 0;
-
- @media (max-width: $screen-xs-max) {
- flex: 1 1 auto;
+ > a {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: $border-radius-default;
+ height: 32px;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ fill: currentColor;
}
}
+
+ &.line-separator {
+ margin: 8px;
+ }
+ }
+}
+
+.navbar-sub-nav {
+ display: -webkit-flex;
+ display: flex;
+ margin: 0 0 0 6px;
+
+ .projects-dropdown-menu {
+ padding: 0;
+ }
+
+ .dropdown-chevron {
+ position: relative;
+ top: -1px;
+ font-size: 10px;
}
.project-item-select-holder {
@@ -315,8 +344,123 @@ header {
}
}
-.with-performance-bar header.navbar-gitlab {
- top: $performance-bar-height;
+.caret-down {
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
+}
+
+.header-user .dropdown-menu-nav,
+.header-new .dropdown-menu-nav {
+ margin-top: $dropdown-vertical-offset;
+}
+
+.breadcrumbs {
+ display: -webkit-flex;
+ display: flex;
+ min-height: 48px;
+ color: $gl-text-color;
+}
+
+.breadcrumbs-container {
+ display: -webkit-flex;
+ display: flex;
+ width: 100%;
+ position: relative;
+ padding-top: $gl-padding / 2;
+ padding-bottom: $gl-padding / 2;
+ align-items: center;
+ border-bottom: 1px solid $border-color;
+}
+
+.breadcrumbs-links {
+ -webkit-flex: 1;
+ flex: 1;
+ min-width: 0;
+ align-self: center;
+ color: $gl-text-color-secondary;
+
+ .avatar-tile {
+ margin-right: 4px;
+ border: 1px solid $border-color;
+ border-radius: 50%;
+ vertical-align: sub;
+ }
+
+ .text-expander {
+ margin-left: 0;
+ margin-right: 2px;
+
+ > i {
+ position: relative;
+ top: 1px;
+ }
+ }
+}
+
+.breadcrumbs-list {
+ display: -webkit-flex;
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 0;
+ line-height: 16px;
+
+ > li {
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding: 2px 0;
+
+ &:not(:last-child) {
+ margin-right: 20px;
+ }
+
+ > a {
+ font-size: 12px;
+ color: currentColor;
+ }
+ }
+}
+
+.breadcrumb-item-text {
+ @include str-truncated(128px);
+ text-decoration: inherit;
+}
+
+.breadcrumbs-list-angle {
+ position: absolute;
+ right: -12px;
+ top: 50%;
+ color: $gl-text-color-tertiary;
+ transform: translateY(-50%);
+}
+
+.breadcrumbs-extra {
+ display: -webkit-flex;
+ display: flex;
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.breadcrumbs-sub-title {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 16px;
+
+ a {
+ color: $gl-text-color;
+ }
+}
+
+.btn-sign-in {
+ margin-top: 3px;
+ font-weight: $gl-font-weight-bold;
+
+ &:hover {
+ background-color: $white-light;
+ }
}
.navbar-nav {
@@ -348,11 +492,10 @@ header {
}
@media (max-width: $screen-xs-max) {
- header .container-fluid {
+ .navbar-gitlab .container-fluid {
font-size: 18px;
.navbar-nav {
- display: table;
table-layout: fixed;
width: 100%;
margin: 0;
@@ -360,7 +503,8 @@ header {
}
.navbar-collapse {
- padding-left: 5px;
+ margin-left: -8px;
+ margin-right: -10px;
.nav > li:not(.hidden-xs) {
display: table-cell !important;
@@ -386,11 +530,11 @@ header {
.dropdown-menu-nav {
width: auto;
min-width: 140px;
- margin-top: -5px;
+ margin-top: 4px;
color: $gl-text-color;
left: auto;
- .current-user {
+ li.current-user {
padding: 5px 18px;
.user-name {
@@ -406,3 +550,23 @@ header {
border-radius: 50%;
border: 1px solid $avatar-border;
}
+
+.with-performance-bar .navbar-gitlab {
+ top: $performance-bar-height;
+}
+
+.navbar-empty {
+ height: $header-height;
+ background: $white-light;
+ border-bottom: 1px solid $white-normal;
+
+ .center-logo {
+ margin: 8px 0;
+ text-align: center;
+
+ .tanuki-logo,
+ img {
+ height: 36px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 09a569ad415..6819fd88b7f 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -9,3 +9,30 @@
padding: 10px;
margin-bottom: 10px;
}
+
+.svg-content {
+ text-align: center;
+ padding: $gl-padding;
+
+ svg,
+ img {
+ max-width: 425px;
+ width: 100%;
+ }
+}
+
+@mixin svg-size($size) {
+ width: $size;
+ height: $size;
+}
+
+svg {
+ &.s8 { @include svg-size(8px); }
+ &.s12 { @include svg-size(12px); }
+ &.s16 { @include svg-size(16px); }
+ &.s18 { @include svg-size(18px); }
+ &.s24 { @include svg-size(24px); }
+ &.s32 { @include svg-size(32px); }
+ &.s48 { @include svg-size(48px); }
+ &.s72 { @include svg-size(72px); }
+}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index bd521028c44..69d19ea2962 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -25,10 +25,6 @@ body {
.content-wrapper {
padding-bottom: 100px;
-
- &:not(.page-with-layout-nav) {
- margin-top: $header-height;
- }
}
.container {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 0fb19344510..badc7b0eba3 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -229,6 +229,10 @@ ul.content-list {
.label-default {
color: $gl-text-color-secondary;
}
+
+ .avatar-cell {
+ align-self: flex-start;
+ }
}
.panel > .content-list > li {
diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss
index b573052c14a..89c561479cc 100644
--- a/app/assets/stylesheets/framework/media_object.scss
+++ b/app/assets/stylesheets/framework/media_object.scss
@@ -6,3 +6,7 @@
.media-body {
flex: 1;
}
+
+.media-body-wrap {
+ flex-grow: 1;
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index d40b65bb2cc..2fee2164190 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -142,5 +142,41 @@
}
@mixin green-status-color {
- @include status-color($green-50, $green-500, $green-700);
+ @include status-color($green-100, $green-500, $green-700);
+}
+
+@mixin fade($gradient-direction, $gradient-color) {
+ visibility: hidden;
+ opacity: 0;
+ z-index: 2;
+ position: absolute;
+ bottom: 12px;
+ width: 43px;
+ height: 30px;
+ transition-duration: .3s;
+ -webkit-transform: translateZ(0);
+ background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
+
+ &.scrolling {
+ visibility: visible;
+ opacity: 1;
+ transition-duration: .3s;
+ }
+
+ .fa {
+ position: relative;
+ top: 5px;
+ font-size: 18px;
+ }
+}
+
+@mixin scrolling-links() {
+ overflow-x: auto;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ display: flex;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 5b581780447..1cebd02df48 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,10 +1,17 @@
+.modal-header {
+ padding: #{3 * $grid-size} #{2 * $grid-size};
+
+ .page-title {
+ margin-top: 0;
+ }
+}
+
.modal-body {
position: relative;
- padding: 15px;
+ padding: #{3 * $grid-size} #{2 * $grid-size};
.form-actions {
- margin: -$gl-padding + 1;
- margin-top: 15px;
+ margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
}
.text-danger {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
deleted file mode 100644
index 5ffa67a1220..00000000000
--- a/app/assets/stylesheets/framework/nav.scss
+++ /dev/null
@@ -1,585 +0,0 @@
-@mixin fade($gradient-direction, $gradient-color) {
- visibility: hidden;
- opacity: 0;
- z-index: 2;
- position: absolute;
- bottom: 12px;
- width: 43px;
- height: 30px;
- transition-duration: .3s;
- -webkit-transform: translateZ(0);
- background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
-
- &.scrolling {
- visibility: visible;
- opacity: 1;
- transition-duration: .3s;
- }
-
- .fa {
- position: relative;
- top: 5px;
- font-size: 18px;
- }
-}
-
-@mixin scrolling-links() {
- overflow-x: auto;
- overflow-y: hidden;
- -webkit-overflow-scrolling: touch;
- display: flex;
-
- &::-webkit-scrollbar {
- display: none;
- }
-}
-
-.nav-links {
- display: flex;
- padding: 0;
- margin: 0;
- list-style: none;
- height: auto;
- border-bottom: 1px solid $border-color;
-
- li {
- display: flex;
-
- a {
- padding: $gl-btn-padding;
- padding-bottom: 11px;
- font-size: 14px;
- line-height: 28px;
- color: $gl-text-color-secondary;
- border-bottom: 2px solid transparent;
- white-space: nowrap;
-
- &:hover,
- &:active,
- &:focus {
- text-decoration: none;
- border-bottom: 2px solid $gray-darkest;
- color: $black;
-
- .badge {
- color: $black;
- }
- }
- }
-
- &.active a {
- border-bottom: 2px solid $link-underline-blue;
- color: $black;
- font-weight: $gl-font-weight-bold;
-
- .badge {
- color: $black;
- }
- }
- }
-
- &.sub-nav {
- text-align: center;
- background-color: $gray-normal;
-
- .container-fluid {
- background-color: $gray-normal;
- margin-bottom: 0;
- display: flex;
- }
-
- li {
- &.active a {
- border-bottom: none;
- color: $link-underline-blue;
- }
-
- a {
- margin: 0;
- padding: 11px 10px 9px;
-
- &:hover,
- &:active,
- &:focus {
- border-color: transparent;
- }
- }
- }
- }
-}
-
-.top-area {
- @include clearfix;
- border-bottom: 1px solid $border-color;
-
- .nav-text {
- padding-top: 16px;
- padding-bottom: 11px;
- display: inline-block;
- line-height: 28px;
- white-space: normal;
-
- /* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
- width: 100%;
- }
- }
-
- .nav-search {
- display: inline-block;
- width: 100%;
- padding: 11px 0;
-
- /* Small devices (phones, tablets, 768px and lower) */
- @media (min-width: $screen-sm-min) {
- width: 50%;
- }
- }
-
- .nav-links {
- margin-bottom: 0;
- border-bottom: none;
- float: left;
-
- &.wide {
- width: 100%;
- display: block;
- }
-
- &.scrolling-tabs {
- float: left;
- }
-
- li a {
- padding: 16px 15px 11px;
- }
-
- /* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
- width: 100%;
- }
- }
-
- .nav-controls {
- @include new-style-dropdown;
-
- display: inline-block;
- float: right;
- text-align: right;
- padding: 11px 0;
- margin-bottom: 0;
-
- > .btn,
- > .btn-container,
- > .dropdown,
- > input,
- > form {
- margin-right: $gl-padding-top;
- display: inline-block;
- vertical-align: top;
-
- &:last-child {
- margin-right: 0;
- float: right;
- }
- }
-
- &.nav-controls-new-nav {
- > .dropdown {
- margin-right: 0;
- }
- }
-
- > .btn-grouped {
- float: none;
- }
-
- .icon-label {
- display: none;
- }
-
- input {
- display: inline-block;
- position: relative;
-
- /* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 200px; }
-
- /* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 250px; }
-
- &.input-short {
- /* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 170px; }
-
- /* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 210px; }
- }
- }
-
- @media (max-width: $screen-xs-max) {
- padding-bottom: 0;
- width: 100%;
-
- .btn,
- form,
- .dropdown,
- .dropdown-toggle,
- .dropdown-menu-toggle,
- .form-control {
- margin: 0 0 10px;
- display: block;
- width: 100%;
- }
-
- form {
- display: block;
- height: auto;
- margin-bottom: 14px;
-
- input {
- width: 100%;
- margin: 0 0 10px;
- }
- }
-
- .input-short {
- width: 100%;
- }
-
- .icon-label {
- display: inline-block;
- }
-
- // Applies on /dashboard/issues
- .project-item-select-holder {
- margin: 0;
- }
- }
- }
-
- &.adjust {
- .nav-text,
- .nav-controls {
- width: auto;
-
- @media (max-width: $screen-xs-max) {
- width: 100%;
- }
- }
- }
-
- &.multi-line {
- .nav-text {
- line-height: 20px;
- }
-
- .nav-controls {
- padding: 17px 0;
- }
- }
-
- pre {
- width: 100%;
- }
-}
-
-.project-item-select-holder.btn-group {
- display: flex;
- max-width: 350px;
- overflow: hidden;
- float: right;
-
- .new-project-item-link {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .new-project-item-select-button {
- width: 32px;
- }
-}
-
-.empty-state .project-item-select-holder.btn-group {
- float: none;
- display: inline-block;
-
- .btn {
- // overrides styles applied to plain `.empty-state .btn`
- margin: 10px 0;
- max-width: 300px;
- width: auto;
-
- @media(max-width: $screen-xs-max) {
- max-width: 250px;
- }
-
- }
-}
-
-.new-project-item-select-button .fa-caret-down {
- margin-left: 2px;
-}
-
-.layout-nav {
- width: 100%;
- background: $gray-light;
- border-bottom: 1px solid $border-color;
- transition: padding $sidebar-transition-duration;
- text-align: center;
- margin-top: $header-height;
-
- .container-fluid {
- position: relative;
-
- .nav-control {
- @media (max-width: $screen-sm-max) {
- margin-right: 2px;
- }
- }
- }
-
- .controls {
- float: right;
- padding: 7px 0 0;
-
- i {
- color: $layout-link-gray;
- }
-
- .fa-rss,
- .fa-cog {
- font-size: 16px;
- }
-
- .fa-caret-down {
- margin-left: 5px;
- color: $gl-text-color-secondary;
- }
-
- .dropdown {
- position: absolute;
- top: 7px;
- right: 15px;
- z-index: 300;
-
- li.active {
- font-weight: $gl-font-weight-bold;
- }
- }
- }
-
- .nav-links {
- border-bottom: none;
- height: 51px;
-
- @media (min-width: $screen-sm-min) {
- justify-content: center;
- }
-
- li {
- a {
- padding-top: 10px;
- }
- }
- }
-}
-
-.with-performance-bar .layout-nav {
- margin-top: $header-height + $performance-bar-height;
-}
-
-.scrolling-tabs-container {
- position: relative;
-
- .merge-request-tabs-container & {
- overflow: hidden;
- }
-
- .nav-links {
- @include scrolling-links();
- }
-
- .fade-right {
- @include fade(left, $gray-light);
- right: -5px;
-
- .fa {
- right: -7px;
- }
- }
-
- .fade-left {
- @include fade(right, $gray-light);
- left: -5px;
- text-align: center;
-
- .fa {
- left: -7px;
- }
- }
-
- &.sub-nav-scroll {
-
- .fade-right {
- @include fade(left, $gray-normal);
- right: 0;
-
- .fa {
- right: -23px;
- }
- }
-
- .fade-left {
- @include fade(right, $gray-normal);
- left: 0;
-
- .fa {
- left: 10px;
- }
- }
- }
-}
-
-.nav-block {
- position: relative;
-
- .nav-links {
- @include scrolling-links();
-
- .fade-right {
- @include fade(left, $white-light);
- right: -5px;
-
- .fa {
- right: -7px;
- }
- }
-
- .fade-left {
- @include fade(right, $white-light);
- left: -5px;
-
- .fa {
- left: -7px;
- }
- }
- }
-}
-
-.page-with-layout-nav {
- .right-sidebar {
- top: ($header-height + 1) * 2;
- }
-
- &.page-with-sub-nav {
- .right-sidebar {
- top: ($header-height + 1) * 3;
-
- &.affix {
- top: $header-height;
- }
- }
- }
-}
-
-.with-performance-bar .page-with-layout-nav {
- .right-sidebar {
- top: ($header-height + 1) * 2 + $performance-bar-height;
- }
-
- &.page-with-sub-nav {
- .right-sidebar {
- top: ($header-height + 1) * 3 + $performance-bar-height;
-
- &.affix {
- top: $header-height + $performance-bar-height;
- }
- }
- }
-}
-
-.nav-block {
- &.activities {
- border-bottom: 1px solid $border-color;
-
- .nav-links {
- border-bottom: none;
- }
- }
-}
-
-@media (max-width: $screen-xs-max) {
- .top-area {
- flex-flow: row wrap;
-
- .nav-controls {
- $controls-margin: $btn-xs-side-margin - 2px;
- flex: 0 0 100%;
-
- &.controls-flex {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: center;
- padding: 0 0 $gl-padding-top;
- }
-
- .controls-item,
- .controls-item-full,
- .controls-item:last-child {
- flex: 1 1 35%;
- display: block;
- width: 100%;
- margin: $controls-margin;
-
- .btn,
- .dropdown {
- margin: 0;
- }
- }
-
- .controls-item-full {
- flex: 1 1 100%;
- }
- }
- }
-}
-
-.inner-page-scroll-tabs {
- position: relative;
-
- .fade-right {
- @include fade(left, $white-light);
- right: 0;
- text-align: right;
-
- .fa {
- right: 5px;
- }
- }
-
- .fade-left {
- @include fade(right, $white-light);
- left: 0;
- text-align: left;
-
- .fa {
- left: 5px;
- }
- }
-
- .fade-right,
- .fade-left {
- top: 16px;
- bottom: auto;
- }
-
- &.is-smaller {
- .fade-right,
- .fade-left {
- top: 11px;
- }
- }
-}
diff --git a/lib/ci/assets/.gitkeep b/app/assets/stylesheets/framework/new-nav.scss
index e69de29bb2d..e69de29bb2d 100644
--- a/lib/ci/assets/.gitkeep
+++ b/app/assets/stylesheets/framework/new-nav.scss
diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss
new file mode 100644
index 00000000000..caf4c7a40b1
--- /dev/null
+++ b/app/assets/stylesheets/framework/new-sidebar.scss
@@ -0,0 +1,510 @@
+@import "framework/variables";
+@import 'framework/tw_bootstrap_variables';
+@import "bootstrap/variables";
+
+$active-background: rgba(0, 0, 0, .04);
+$active-hover-background: $active-background;
+$active-hover-color: $gl-text-color;
+$inactive-badge-background: rgba(0, 0, 0, .08);
+$hover-background: $white-light;
+$hover-color: $gl-text-color;
+$inactive-color: $gl-text-color-secondary;
+$new-sidebar-width: 220px;
+$new-sidebar-collapsed-width: 50px;
+
+.page-with-new-sidebar {
+ @media (min-width: $screen-md-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ padding-left: $new-sidebar-width;
+ }
+
+ // Override position: absolute
+ .right-sidebar {
+ position: fixed;
+ height: calc(100% - #{$header-height});
+ }
+
+ .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
+ padding: 10px 0 15px;
+ }
+}
+
+.page-with-icon-sidebar {
+ @media (min-width: $screen-sm-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+}
+
+.context-header {
+ position: relative;
+ margin-right: 2px;
+
+ a {
+ font-weight: $gl-font-weight-bold;
+ display: flex;
+ align-items: center;
+ padding: 10px 16px 10px 10px;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ a:hover {
+ background-color: $hover-background;
+ color: $hover-color;
+
+ .settings-avatar {
+ svg {
+ fill: $hover-color;
+ }
+ }
+ }
+
+ .avatar-container {
+ flex: 0 0 40px;
+ background-color: $white-light;
+ }
+
+ .sidebar-context-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.settings-avatar {
+ background-color: $white-light;
+
+ svg {
+ fill: $gl-text-color-secondary;
+ margin: auto;
+ }
+}
+
+.nav-sidebar {
+ position: fixed;
+ z-index: 400;
+ width: $new-sidebar-width;
+ transition: left $sidebar-transition-duration;
+ top: $header-height;
+ bottom: 0;
+ left: 0;
+ background-color: $gray-normal;
+ box-shadow: inset -2px 0 0 $border-color;
+ transform: translate3d(0, 0, 0);
+
+ &:not(.sidebar-icons-only) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ box-shadow: inset -2px 0 0 $border-color,
+ 2px 1px 3px $dropdown-shadow-color;
+ }
+ }
+
+ &.sidebar-icons-only {
+ width: auto;
+ min-width: $new-sidebar-collapsed-width;
+
+ .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+ }
+
+ .badge:not(.fly-out-badge),
+ .sidebar-context-title,
+ .nav-item-name {
+ display: none;
+ }
+
+ .sidebar-top-level-items > li > a {
+ min-height: 44px;
+ }
+
+ .fly-out-top-item {
+ display: block;
+ }
+
+ .avatar-container {
+ margin-right: 0;
+ }
+ }
+
+ &.nav-sidebar-expanded {
+ left: 0;
+ }
+
+ a {
+ transition: none;
+ text-decoration: none;
+ }
+
+ ul {
+ padding-left: 0;
+ list-style: none;
+ }
+
+ li {
+ white-space: nowrap;
+
+ a {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: $inactive-color;
+ }
+
+ svg {
+ fill: $inactive-color;
+ }
+ }
+
+ .nav-item-name {
+ flex: 1;
+ }
+
+ li.active {
+ > a {
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ left: (-$new-sidebar-width);
+ }
+
+ .nav-icon-container {
+ display: flex;
+ margin-right: 8px;
+ }
+
+ .fly-out-top-item {
+ display: none;
+ }
+
+ svg {
+ height: 16px;
+ width: 16px;
+ }
+}
+
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+
+ @media (min-width: $screen-sm-min) {
+ overflow: hidden;
+ }
+}
+
+.with-performance-bar .nav-sidebar {
+ top: $header-height + $performance-bar-height;
+}
+
+.sidebar-sub-level-items {
+ display: none;
+ padding-bottom: 8px;
+
+ > li {
+ a {
+ padding: 8px 16px 8px 40px;
+
+ &:hover,
+ &:focus {
+ background: $active-hover-background;
+ color: $active-hover-color;
+ }
+ }
+
+ &.active {
+ a {
+ &,
+ &:hover,
+ &:focus {
+ background: $active-background;
+ }
+ }
+ }
+ }
+}
+
+.sidebar-top-level-items {
+ margin-bottom: 60px;
+
+ > li {
+ > a {
+ @media (min-width: $screen-sm-min) {
+ margin-right: 2px;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+ }
+
+ &.is-showing-fly-out {
+ > a {
+ margin-right: 2px;
+ }
+
+ .sidebar-sub-level-items {
+ @media (min-width: $screen-sm-min) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ min-width: 150px;
+ margin-top: -1px;
+ padding: 4px 1px;
+ background-color: $white-light;
+ box-shadow: 2px 1px 3px $dropdown-shadow-color;
+ border: 1px solid $gray-darker;
+ border-left: 0;
+ border-radius: 0 3px 3px 0;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: -30px;
+ bottom: -30px;
+ left: -10px;
+ right: -30px;
+ z-index: -1;
+ }
+
+ &.is-above {
+ margin-top: 1px;
+ }
+
+ .divider {
+ height: 1px;
+ margin: 4px -1px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
+
+ > .active {
+ box-shadow: none;
+
+ > a {
+ background-color: transparent;
+ }
+ }
+
+ a {
+ padding: 8px 16px;
+ color: $gl-text-color;
+
+ &:hover,
+ &:focus {
+ background-color: $gray-darker;
+ }
+ }
+ }
+ }
+ }
+
+ .badge {
+ background-color: $inactive-badge-background;
+ color: $inactive-color;
+ }
+
+ &.active {
+ background: $active-background;
+
+ > a {
+ margin-left: 4px;
+ padding-left: 12px;
+ }
+
+ .badge {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .sidebar-sub-level-items:not(.is-fly-out-only) {
+ display: block;
+ }
+ }
+
+ &.active > a:hover,
+ &.is-over > a {
+ background-color: $white-light;
+ }
+ }
+}
+
+
+// Collapsed nav
+
+.toggle-sidebar-button,
+.close-nav-button {
+ width: $new-sidebar-width - 2px;
+ position: fixed;
+ bottom: 0;
+ padding: 16px;
+ background-color: $gray-normal;
+ border: 0;
+ border-top: 2px solid $border-color;
+ color: $gl-text-color-secondary;
+ display: flex;
+ align-items: center;
+
+ svg {
+ fill: $gl-text-color-secondary;
+ margin-right: 8px;
+ }
+
+ .icon-angle-double-right {
+ display: none;
+ }
+
+ &:hover {
+ background-color: $border-color;
+ color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+}
+
+.toggle-sidebar-button {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+}
+
+
+.sidebar-icons-only {
+ .context-header {
+ height: 61px;
+
+ a {
+ padding: 10px 4px;
+ }
+ }
+
+ li a {
+ padding: 12px 15px;
+ }
+
+ .sidebar-top-level-items > li {
+ &.active a {
+ padding-left: 12px;
+ }
+
+ .sidebar-sub-level-items {
+ &:not(.flyout-list) {
+ display: none;
+ }
+ }
+ }
+
+ .nav-icon-container {
+ margin-right: 0;
+ }
+
+ .toggle-sidebar-button {
+ width: $new-sidebar-collapsed-width - 2px;
+ padding: 16px;
+
+ .collapse-text,
+ .icon-angle-double-left {
+ display: none;
+ }
+
+ .icon-angle-double-right {
+ display: block;
+ margin: 0;
+ }
+ }
+}
+
+.fly-out-top-item {
+ > a {
+ display: flex;
+ }
+
+ .fly-out-badge {
+ margin-left: 8px;
+ }
+}
+
+.fly-out-top-item-name {
+ flex: 1;
+}
+
+// Mobile nav
+
+.close-nav-button {
+ display: none;
+}
+
+.toggle-mobile-nav {
+ display: none;
+ background-color: transparent;
+ border: 0;
+ padding: 6px 16px;
+ margin: 0 0 0 -15px;
+ height: 46px;
+
+ i {
+ font-size: 20px;
+ color: $gl-text-color-secondary;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ display: flex;
+ align-items: center;
+
+ i {
+ font-size: 18px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ + .breadcrumbs-links {
+ padding-left: 17px;
+ border-left: 1px solid $gl-text-color-quaternary;
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .close-nav-button {
+ display: flex;
+ }
+}
+
+.mobile-overlay {
+ display: none;
+
+ &.mobile-nav-open {
+ display: block;
+ position: fixed;
+ background-color: $black-transparent;
+ height: 100%;
+ width: 100%;
+ z-index: 300;
+ }
+}
+
+
+// Make issue boards full-height now that sub-nav is gone
+
+.boards-list {
+ height: calc(100vh - #{$header-height});
+
+ @media (min-width: $screen-sm-min) {
+ height: 475px; // Needed for PhantomJS
+ // scss-lint:disable DuplicateProperty
+ height: calc(100vh - 180px);
+ // scss-lint:enable DuplicateProperty
+ }
+}
+
+.with-performance-bar .boards-list {
+ height: calc(100vh - #{$header-height} - #{$performance-bar-height});
+}
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
new file mode 100644
index 00000000000..5c96b3b78e7
--- /dev/null
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -0,0 +1,376 @@
+// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
+// please check nav.scss
+.nav-links {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+
+ li {
+ display: flex;
+
+ a {
+ padding: $gl-btn-padding;
+ padding-bottom: 11px;
+ font-size: 14px;
+ line-height: 28px;
+ color: $gl-text-color-secondary;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+
+ &:hover,
+ &:active,
+ &:focus {
+ text-decoration: none;
+ color: $black;
+
+ .badge {
+ color: $black;
+ }
+ }
+ }
+
+ &.active a {
+ color: $black;
+ font-weight: $gl-font-weight-bold;
+
+ .badge {
+ color: $black;
+ }
+ }
+ }
+}
+
+.top-area {
+ @include clearfix;
+ border-bottom: 1px solid $border-color;
+
+ .nav-text {
+ padding-top: 16px;
+ padding-bottom: 11px;
+ display: inline-block;
+ line-height: 28px;
+ white-space: normal;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
+ }
+
+ .nav-links {
+ margin-bottom: 0;
+ border-bottom: none;
+ float: left;
+
+ &.wide {
+ width: 100%;
+ display: block;
+ }
+
+ &.scrolling-tabs {
+ float: left;
+ }
+
+ li a {
+ padding: 16px 15px 11px;
+ }
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
+ }
+
+ .nav-controls {
+ @include new-style-dropdown;
+
+ display: inline-block;
+ float: right;
+ text-align: right;
+ padding: 11px 0;
+ margin-bottom: 0;
+
+ > .btn,
+ > .btn-container,
+ > .dropdown,
+ > input,
+ > form {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+ vertical-align: top;
+
+ &:last-child {
+ margin-right: 0;
+ float: right;
+ }
+ }
+
+ > .btn-grouped {
+ float: none;
+ }
+
+ .icon-label {
+ display: none;
+ }
+
+ input {
+ display: inline-block;
+ position: relative;
+
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 200px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 250px; }
+
+ &.input-short {
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 170px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 210px; }
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ padding-bottom: 0;
+ width: 100%;
+
+ .btn,
+ form,
+ .dropdown,
+ .dropdown-toggle,
+ .dropdown-menu-toggle,
+ .form-control {
+ margin: 0 0 10px;
+ display: block;
+ width: 100%;
+ }
+
+ form {
+ display: block;
+ height: auto;
+ margin-bottom: 14px;
+
+ input {
+ width: 100%;
+ margin: 0 0 10px;
+ }
+ }
+
+ .input-short {
+ width: 100%;
+ }
+
+ .icon-label {
+ display: inline-block;
+ }
+
+ // Applies on /dashboard/issues
+ .project-item-select-holder {
+ margin: 0;
+ }
+ }
+ }
+
+ &.adjust {
+ .nav-text,
+ .nav-controls {
+ width: auto;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
+ }
+ }
+
+ &.multi-line {
+ .nav-text {
+ line-height: 20px;
+ }
+
+ .nav-controls {
+ padding: 17px 0;
+ }
+ }
+
+ pre {
+ width: 100%;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ flex-flow: row wrap;
+
+ .nav-controls {
+ $controls-margin: $btn-xs-side-margin - 2px;
+ flex: 0 0 100%;
+
+ &.controls-flex {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 $gl-padding-top;
+ }
+
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
+
+ .btn,
+ .dropdown {
+ margin: 0;
+ }
+ }
+
+ .controls-item-full {
+ flex: 1 1 100%;
+ }
+ }
+ }
+}
+
+.scrolling-tabs-container {
+ position: relative;
+
+ .merge-request-tabs-container & {
+ overflow: hidden;
+ }
+
+ .nav-links {
+ @include scrolling-links();
+ }
+
+ .fade-right {
+ @include fade(left, $gray-light);
+ right: -5px;
+
+ .fa {
+ right: -7px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, $gray-light);
+ left: -5px;
+ text-align: center;
+
+ .fa {
+ left: -7px;
+ }
+ }
+}
+
+.inner-page-scroll-tabs {
+ position: relative;
+
+ .fade-right {
+ @include fade(left, $white-light);
+ right: 0;
+ text-align: right;
+
+ .fa {
+ right: 5px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, $white-light);
+ left: 0;
+ text-align: left;
+
+ .fa {
+ left: 5px;
+ }
+ }
+
+ .fade-right,
+ .fade-left {
+ top: 16px;
+ bottom: auto;
+ }
+
+ &.is-smaller {
+ .fade-right,
+ .fade-left {
+ top: 11px;
+ }
+ }
+}
+
+.nav-block {
+ position: relative;
+
+ .nav-links {
+ @include scrolling-links();
+
+ .fade-right {
+ @include fade(left, $white-light);
+ right: -5px;
+
+ .fa {
+ right: -7px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, $white-light);
+ left: -5px;
+
+ .fa {
+ left: -7px;
+ }
+ }
+ }
+
+ &.activities {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border-bottom: none;
+ }
+ }
+}
+
+.project-item-select-holder.btn-group {
+ display: flex;
+ max-width: 350px;
+ overflow: hidden;
+ float: right;
+
+ .new-project-item-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .new-project-item-select-button {
+ width: 32px;
+ }
+}
+
+.empty-state .project-item-select-holder.btn-group {
+ float: none;
+ display: inline-block;
+
+ .btn {
+ // overrides styles applied to plain `.empty-state .btn`
+ margin: 10px 0;
+ max-width: 300px;
+ width: auto;
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 250px;
+ }
+ }
+}
+
+.new-project-item-select-button .fa-caret-down {
+ margin-left: 2px;
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 6c14e8b97e0..50f1445bc2e 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -48,31 +48,24 @@
}
&:hover {
- background-color: $white-normal;
- border-color: $border-white-normal;
+ border-color: $gray-darkest;
color: $gl-text-color;
}
}
}
-.select2-drop {
- box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
- border-radius: $border-radius-default;
- border: none;
+.select2-drop,
+.select2-drop.select2-drop-above {
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+ border-radius: $border-radius-base;
+ border: 1px solid $dropdown-border-color;
min-width: 175px;
+ color: $gl-text-color;
}
-.select2-results .select2-result-label,
-.select2-more-results {
- padding: 10px 15px;
-}
-
-.select2-drop {
- color: $gl-grayish-blue;
-}
-
-.select2-highlighted {
- background: $gl-link-color !important;
+.select2-drop.select2-drop-above.select2-drop-active {
+ border-top: 1px solid $dropdown-border-color;
+ margin-top: -6px;
}
.select2-results li.select2-result-with-children > .select2-result-label {
@@ -87,13 +80,11 @@
}
}
-.select2-dropdown-open {
+.select2-dropdown-open,
+.select2-dropdown-open.select2-drop-above {
.select2-choice {
- border-color: $border-white-normal;
+ border-color: $gray-darkest;
outline: 0;
- background-image: none;
- background-color: $white-dark;
- box-shadow: $gl-btn-active-gradient;
}
}
@@ -131,28 +122,14 @@
}
}
}
-
- &.select2-container-active .select2-choices,
- &.select2-dropdown-open .select2-choices {
- border-color: $border-white-normal;
- box-shadow: $gl-btn-active-gradient;
- }
}
.select2-drop-active {
- margin-top: 6px;
+ margin-top: $dropdown-vertical-offset;
font-size: 14px;
- &.select2-drop-above {
- margin-bottom: 8px;
- }
-
.select2-results {
max-height: 350px;
-
- .select2-highlighted {
- background: $gl-primary;
- }
}
}
@@ -186,19 +163,35 @@
background-size: 16px 16px !important;
}
-.select2-results .select2-no-results,
-.select2-results .select2-searching,
-.select2-results .select2-ajax-error,
-.select2-results .select2-selection-limit {
- background: $gray-light;
- display: list-item;
- padding: 10px 15px;
-}
-
-
.select2-results {
margin: 0;
- padding: 10px 0;
+ padding: #{$gl-padding / 2} 0;
+
+ .select2-no-results,
+ .select2-searching,
+ .select2-ajax-error,
+ .select2-selection-limit {
+ background: transparent;
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-result-label,
+ .select2-more-results {
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-highlighted {
+ background: transparent;
+ color: $gl-text-color;
+
+ .select2-result-label {
+ background: $dropdown-item-hover-bg;
+ }
+ }
+
+ .select2-result {
+ padding: 0 1px;
+ }
}
.ajax-users-select {
@@ -265,56 +258,10 @@
min-width: 250px !important;
}
-// TODO: change global style
-.ajax-project-dropdown,
-.ajax-users-dropdown,
-body[data-page="projects:edit"] #select2-drop,
-body[data-page="projects:new"] #select2-drop,
-body[data-page="projects:merge_requests:edit"] #select2-drop,
-body[data-page="projects:blob:new"] #select2-drop,
-body[data-page="profiles:show"] #select2-drop,
-body[data-page="admin:groups:show"] #select2-drop,
-body[data-page="projects:issues:show"] #select2-drop,
-body[data-page="projects:blob:edit"] #select2-drop {
- &.select2-drop {
- border: 1px solid $dropdown-border-color;
- border-radius: $border-radius-base;
- color: $gl-text-color;
- }
-
- &.select2-drop-above {
- border-top: none;
- margin-top: -4px;
- }
-
- .select2-results {
- .select2-no-results,
- .select2-searching,
- .select2-ajax-error,
- .select2-selection-limit {
- background: transparent;
- }
-
- .select2-result {
- padding: 0 1px;
-
- .select2-match {
- font-weight: $gl-font-weight-bold;
- text-decoration: none;
- }
-
- .select2-result-label {
- padding: #{$gl-padding / 2} $gl-padding;
- }
-
- &.select2-highlighted {
- background-color: transparent !important;
- color: $gl-text-color;
-
- .select2-result-label {
- background-color: $dropdown-item-hover-bg;
- }
- }
- }
+.select2-result-selectable,
+.select2-result-unselectable {
+ .select2-match {
+ font-weight: $gl-font-weight-bold;
+ text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 3d68a50f91f..f718ec4bcad 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -17,15 +17,19 @@
.diff-file {
border: 1px solid $border-color;
- border-bottom: none;
margin: 0;
}
+
+ &.text-file .diff-file {
+ border-bottom: none;
+ }
}
.timeline-entry {
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
+ background: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss
new file mode 100644
index 00000000000..93baf73cb78
--- /dev/null
+++ b/app/assets/stylesheets/framework/tooltips.scss
@@ -0,0 +1,7 @@
+.tooltip-inner {
+ font-size: $tooltip-font-size;
+ border-radius: $border-radius-default;
+ line-height: 16px;
+ font-weight: $gl-font-weight-normal;
+ padding: $gl-btn-padding;
+}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 4c35e3a9c3c..3ea77eb7a43 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -137,7 +137,7 @@ $well-border: #eee;
//##
$code-color: $red-600;
-$code-bg: lighten($red-50, 2%);
+$code-bg: lighten($red-100, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3857226cddb..bbbd16322eb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,11 +1,14 @@
/*
* Layout
*/
+$grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
+$default-transition-duration: .15s;
+$right-sidebar-transition-duration: .3s;
/*
* Color schema
@@ -13,6 +16,7 @@ $sidebar-breakpoint: 1024px;
$darken-normal-factor: 7%;
$darken-dark-factor: 10%;
$darken-border-factor: 5%;
+$darken-border-dashed-factor: 25%;
$white-light: #fff;
$white-normal: #f0f0f0;
@@ -26,46 +30,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
-$green-25: #f6fcf8;
-$green-50: #e4f5eb;
-$green-100: #bae6cc;
-$green-200: #8dd5aa;
-$green-300: #5fc488;
-$green-400: #3cb76f;
+$green-50: #f1fdf6;
+$green-100: #dcf5e7;
+$green-200: #b3e6c8;
+$green-300: #75d09b;
+$green-400: #37b96d;
$green-500: #1aaa55;
$green-600: #168f48;
$green-700: #12753a;
$green-800: #0e5a2d;
$green-900: #0a4020;
+$green-950: #072b15;
-$blue-25: #f6fafd;
-$blue-50: #e4eff9;
-$blue-100: #bcd7f1;
-$blue-200: #8fbce8;
-$blue-300: #62a1df;
-$blue-400: #418cd8;
+$blue-50: #f6fafe;
+$blue-100: #e4f0fb;
+$blue-200: #b8d6f4;
+$blue-300: #73afea;
+$blue-400: #2e87e0;
$blue-500: #1f78d1;
$blue-600: #1b69b6;
$blue-700: #17599c;
$blue-800: #134a81;
$blue-900: #0f3b66;
+$blue-950: #0a2744;
-$orange-25: #fffcf8;
-$orange-50: #fff2e1;
-$orange-100: #fedfb3;
-$orange-200: #feca81;
-$orange-300: #fdb44f;
-$orange-400: #fca429;
+$orange-50: #fffaf4;
+$orange-100: #fff1de;
+$orange-200: #fed69f;
+$orange-300: #fdbc60;
+$orange-400: #fca121;
$orange-500: #fc9403;
$orange-600: #de7e00;
$orange-700: #c26700;
-$orange-800: #a35100;
-$orange-900: #853b00;
+$orange-800: #a35200;
+$orange-900: #853c00;
+$orange-950: #592800;
-$red-25: #fef7f6;
-$red-50: #fbe7e4;
-$red-100: #f4c4bc;
-$red-200: #ed9d90;
+$red-50: #fef6f5;
+$red-100: #fbe5e1;
+$red-200: #f2b4a9;
$red-300: #e67664;
$red-400: #e05842;
$red-500: #db3b21;
@@ -73,6 +76,7 @@ $red-600: #c0341d;
$red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
+$red-950: #4b140b;
// GitLab themes
@@ -134,6 +138,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor);
$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
+$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
@@ -182,8 +187,8 @@ $list-text-disabled-color: $gl-text-color-tertiary;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
-$list-warning-row-bg: $orange-50;
-$list-warning-row-border: $orange-100;
+$list-warning-row-bg: $orange-100;
+$list-warning-row-border: $orange-200;
$list-warning-row-color: $orange-700;
/*
@@ -199,6 +204,11 @@ $code_font_size: 12px;
$code_line_height: 1.6;
/*
+ * Tooltips
+ */
+$tooltip-font-size: 12px;
+
+/*
* Padding
*/
$gl-padding: 16px;
@@ -212,11 +222,10 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
-$row-hover: $blue-25;
-$row-hover-border: $blue-100;
+$row-hover: $blue-50;
+$row-hover-border: $blue-200;
$progress-color: #c0392b;
-$header-height: 50px;
-$new-navbar-height: 40px;
+$header-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
@@ -258,13 +267,14 @@ $well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
-$user-mention-color: #2fa0bb;
+$user-mention-bg: rgba($blue-500, 0.044);
+$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
$error-bg: $red-400;
-$warning-message-bg: $orange-50;
-$warning-message-border: $orange-100;
+$warning-message-bg: $orange-100;
+$warning-message-border: $orange-200;
$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
@@ -312,6 +322,7 @@ $diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1;
+$diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
@@ -323,6 +334,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
+$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
@@ -449,17 +461,17 @@ $builds-trace-bg: #111;
/*
* Callout
*/
-$callout-danger-bg: $red-50;
-$callout-danger-border: $red-100;
+$callout-danger-bg: $red-100;
+$callout-danger-border: $red-200;
$callout-danger-color: $red-700;
-$callout-warning-bg: $orange-50;
-$callout-warning-border: $orange-100;
+$callout-warning-bg: $orange-100;
+$callout-warning-border: $orange-200;
$callout-warning-color: $orange-700;
-$callout-info-bg: $blue-50;
-$callout-info-border: $blue-100;
+$callout-info-bg: $blue-100;
+$callout-info-border: $blue-200;
$callout-info-color: $blue-700;
-$callout-success-bg: $green-50;
-$callout-success-border: $green-100;
+$callout-success-bg: $green-100;
+$callout-success-border: $green-200;
$callout-success-color: $green-700;
/*
@@ -694,3 +706,15 @@ Project Templates Icons
$rails: #c00;
$node: #353535;
$java: #70ad51;
+
+/*
+Issuable warning
+*/
+$issuable-warning-size: 24px;
+$issuable-warning-icon-margin: 4px;
+
+/*
+Image Commenting cursor
+*/
+$image-comment-cursor-left-offset: 12;
+$image-comment-cursor-top-offset: 30;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
deleted file mode 100644
index 8e095cbdd7e..00000000000
--- a/app/assets/stylesheets/new_nav.scss
+++ /dev/null
@@ -1,468 +0,0 @@
-@import "framework/variables";
-@import 'framework/tw_bootstrap_variables';
-@import "bootstrap/variables";
-@import "framework/mixins";
-
-.content-wrapper.page-with-new-nav {
- margin-top: $new-navbar-height;
-}
-
-header.navbar-gitlab-new {
- color: $white-light;
- border-bottom: 0;
- min-height: $new-navbar-height;
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- .header-content {
- display: -webkit-flex;
- display: flex;
- padding-left: 0;
- min-height: $new-navbar-height;
-
- .title-container {
- display: -webkit-flex;
- display: flex;
- -webkit-align-items: stretch;
- align-items: stretch;
- -webkit-flex: 1 1 auto;
- flex: 1 1 auto;
- padding-top: 0;
- overflow: visible;
- }
-
- .title {
- display: -webkit-flex;
- display: flex;
- padding-right: 0;
- color: currentColor;
-
- img {
- height: 28px;
- margin-right: 8px;
- }
-
- a {
- display: -webkit-flex;
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 5px 2px 5px -8px;
- border-radius: $border-radius-default;
-
- svg {
- @media (min-width: $screen-sm-min) {
- margin-right: 8px;
- }
- }
- }
- }
-
- .dropdown.open {
- > a {
- border-bottom-color: $white-light;
- }
- }
-
- .dropdown-menu {
- margin-top: 4px;
- min-width: 130px;
-
- @media (max-width: $screen-xs-max) {
- left: auto;
- right: 0;
- }
- }
-
- &.menu-expanded {
- @media (max-width: $screen-xs-max) {
- .title-container,
- .header-logo, {
- display: none;
- }
- }
- }
- }
-
- .dropdown-bold-header {
- color: $gl-text-color-secondary;
- font-size: 12px;
- }
-
- .navbar-collapse {
- padding-left: 0;
- box-shadow: 0;
-
- @media (max-width: $screen-xs-max) {
- margin-left: -8px;
- margin-right: -10px;
- }
-
- .nav {
- > li:not(.hidden-xs) a {
- @media (max-width: $screen-xs-max) {
- margin-left: 0;
- min-width: 100%;
- }
- }
- }
- }
-
- .container-fluid {
- .navbar-toggle {
- min-width: 45px;
- padding: 4px $gl-padding;
- margin-right: -7px;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-
- &:hover,
- &:focus,
- &.active {
- color: currentColor;
- background-color: transparent;
- }
- }
-
- .navbar-nav {
- @media (max-width: $screen-xs-max) {
- display: flex;
- padding-right: 10px;
- }
-
- li {
- .badge {
- box-shadow: none;
- font-weight: $gl-font-weight-bold;
- }
- }
- }
-
- .nav > li {
- &.header-user {
- @media (max-width: $screen-xs-max) {
- padding-left: 10px;
- }
- }
-
- > a {
- will-change: color;
- margin: 4px 2px;
- padding: 6px 8px;
- height: 32px;
-
- @media (max-width: $screen-xs-max) {
- padding: 0;
- }
-
- &.header-user-dropdown-toggle {
- margin-left: 2px;
-
- .header-user-avatar {
- margin-right: 0;
- }
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- outline: 0;
- opacity: 1;
- color: $white-light;
-
- svg {
- fill: currentColor;
- }
-
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
- }
- }
- }
-
- .header-new-dropdown-toggle {
- margin-right: 0;
- }
-
- .impersonated-user,
- .impersonated-user:hover {
- margin-right: 1px;
- background-color: $white-light;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- .impersonation-btn,
- .impersonation-btn:hover {
- background-color: $white-light;
- margin-left: 0;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-
- i {
- color: $orange-500;
- font-size: 20px;
- }
- }
-
- &.active > a,
- &.dropdown.open > a {
-
- svg {
- fill: currentColor;
- }
- }
- }
- }
-}
-
-.navbar-sub-nav {
- display: -webkit-flex;
- display: flex;
- margin: 0 0 0 6px;
-
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
-}
-
-.navbar-gitlab-new {
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- color: $white-light;
-
- svg {
- fill: currentColor;
- }
- }
-
- > a {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: $border-radius-default;
- height: 32px;
- font-weight: $gl-font-weight-bold;
-
- svg {
- fill: currentColor;
- }
- }
-
- &.line-separator {
- margin: 8px;
- }
- }
- }
-}
-
-.admin-icon i {
- font-size: 18px;
-}
-
-.caret-down {
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-
-.header-user .dropdown-menu-nav,
-.header-new .dropdown-menu-nav {
- margin-top: 4px;
-}
-
-.search {
- margin: 4px 8px 0;
-
- form {
- height: 32px;
- border: 0;
- border-radius: $border-radius-default;
- transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
-
- &:hover {
- box-shadow: none;
- }
- }
-
- &.search-active form {
- box-shadow: none;
-
- .search-input {
- color: $gl-text-color;
- transition: color ease-in-out 0.15s;
- }
-
- .search-input::placeholder {
- color: $gl-text-color-tertiary;
- }
-
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- color: $gl-text-color-tertiary;
- transition: color ease-in-out 0.15s;
- }
- }
- }
-
- .search-input {
- color: $white-light;
- background: none;
- transition: color ease-in-out 0.15s;
- }
-
- .search-input::placeholder {
- transition: color ease-in-out 0.15s;
- }
-
- .location-badge {
- font-size: 12px;
- margin: -4px 4px -4px -4px;
- line-height: 25px;
- padding: 4px 8px;
- border-radius: 2px 0 0 2px;
- height: 32px;
- transition: border-color ease-in-out 0.15s;
- }
-
- &.search-active {
- .location-badge {
- background-color: $nav-badge-bg;
- border-color: $border-color;
- }
-
- .search-input-wrap {
- .clear-icon {
- color: $white-light;
- }
- }
- }
-}
-
-.breadcrumbs {
- display: flex;
- min-height: 48px;
- color: $gl-text-color;
-}
-
-.breadcrumbs-container {
- display: -webkit-flex;
- display: flex;
- width: 100%;
- position: relative;
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
- align-items: center;
- border-bottom: 1px solid $border-color;
-}
-
-.breadcrumbs-links {
- -webkit-flex: 1;
- flex: 1;
- min-width: 0;
- align-self: center;
- color: $gl-text-color-secondary;
-
- .avatar-tile {
- margin-right: 4px;
- border: 1px solid $border-color;
- border-radius: 50%;
- vertical-align: sub;
- }
-
- .text-expander {
- margin-left: 0;
- margin-right: 2px;
-
- > i {
- position: relative;
- top: 1px;
- }
- }
-}
-
-.breadcrumbs-list {
- display: -webkit-flex;
- display: flex;
- flex-wrap: wrap;
- margin-bottom: 0;
- line-height: 16px;
-
- > li {
- display: flex;
- align-items: center;
- position: relative;
-
- &:not(:last-child) {
- margin-right: 20px;
- }
-
- > a {
- font-size: 12px;
- color: currentColor;
- }
- }
-}
-
-.breadcrumb-item-text {
- @include str-truncated(128px);
-}
-
-.breadcrumbs-list-angle {
- position: absolute;
- right: -12px;
- top: 50%;
- color: $gl-text-color-tertiary;
- transform: translateY(-50%);
-}
-
-.breadcrumbs-extra {
- display: flex;
- flex: 0 0 auto;
- margin-left: auto;
-}
-
-.breadcrumbs-sub-title {
- margin: 0;
- font-size: 12px;
- font-weight: 600;
- line-height: 1;
-
- a {
- color: $gl-text-color;
- }
-}
-
-.btn-sign-in {
- margin-top: 3px;
- font-weight: $gl-font-weight-bold;
-
- &:hover {
- background-color: $white-light;
- }
-}
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
deleted file mode 100644
index 4bbd30056a9..00000000000
--- a/app/assets/stylesheets/new_sidebar.scss
+++ /dev/null
@@ -1,499 +0,0 @@
-@import "framework/variables";
-@import 'framework/tw_bootstrap_variables';
-@import "bootstrap/variables";
-
-$active-background: rgba(0, 0, 0, .04);
-$active-border: $indigo-500;
-$active-color: $indigo-700;
-$active-hover-background: $active-background;
-$active-hover-color: $gl-text-color;
-$inactive-badge-background: rgba(0, 0, 0, .08);
-$hover-background: $white-light;
-$hover-color: $gl-text-color;
-$inactive-color: $gl-text-color-secondary;
-$new-sidebar-width: 220px;
-$new-sidebar-collapsed-width: 50px;
-
-.page-with-new-sidebar {
- @media (min-width: $screen-md-min) {
- padding-left: $new-sidebar-collapsed-width;
- }
-
- @media (min-width: $screen-lg-min) {
- padding-left: $new-sidebar-width;
- }
-
- // Override position: absolute
- .right-sidebar {
- position: fixed;
- height: calc(100% - #{$new-navbar-height});
- }
-
- .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
- padding: 10px 0 15px;
- }
-}
-
-.page-with-icon-sidebar {
- @media (min-width: $screen-sm-min) {
- padding-left: $new-sidebar-collapsed-width;
- }
-}
-
-.context-header {
- position: relative;
- margin-right: 2px;
-
- a {
- font-weight: $gl-font-weight-bold;
- display: flex;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: $gl-text-color;
- }
-
- &:hover,
- a:hover {
- background-color: $hover-background;
- color: $hover-color;
-
- .settings-avatar {
- i {
- color: $hover-color;
- }
- }
- }
-
- .avatar-container {
- flex: 0 0 40px;
- background-color: $white-light;
- }
-
- .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-
-.settings-avatar {
- background-color: $white-light;
-
- i {
- font-size: 20px;
- width: 100%;
- color: $gl-text-color-secondary;
- text-align: center;
- align-self: center;
- }
-}
-
-.nav-sidebar {
- position: fixed;
- z-index: 400;
- width: $new-sidebar-width;
- transition: left $sidebar-transition-duration;
- top: $new-navbar-height;
- bottom: 0;
- left: 0;
- background-color: $gray-normal;
- box-shadow: inset -2px 0 0 $border-color;
- transform: translate3d(0, 0, 0);
-
- &.sidebar-icons-only {
- width: $new-sidebar-collapsed-width;
-
- .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
-
- .badge:not(.fly-out-badge),
- .sidebar-context-title,
- .nav-item-name {
- display: none;
- }
-
- .sidebar-top-level-items > li > a {
- min-height: 44px;
- }
-
- .fly-out-top-item {
- display: block;
- }
- }
-
- &.nav-sidebar-expanded {
- left: 0;
- }
-
- a {
- transition: none;
- text-decoration: none;
- }
-
- ul {
- padding-left: 0;
- list-style: none;
- }
-
- li {
- white-space: nowrap;
-
- a {
- display: flex;
- align-items: center;
- padding: 12px 16px;
- color: $inactive-color;
- }
-
- svg {
- fill: $inactive-color;
- }
- }
-
- .nav-item-name {
- flex: 1;
- }
-
- li.active {
- > a {
- font-weight: $gl-font-weight-bold;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- left: (-$new-sidebar-width);
- }
-
- .nav-icon-container {
- display: flex;
- margin-right: 8px;
-
- svg {
- height: 16px;
- width: 16px;
- }
- }
-
- .fly-out-top-item {
- display: none;
- }
-}
-
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow: auto;
-}
-
-.with-performance-bar .nav-sidebar {
- top: $new-navbar-height + $performance-bar-height;
-}
-
-.sidebar-sub-level-items {
- display: none;
- padding-bottom: 8px;
-
- > li {
- a {
- padding: 8px 16px 8px 40px;
-
- &:hover,
- &:focus {
- background: $active-hover-background;
- color: $active-hover-color;
- }
- }
-
- &.active {
- a {
- &,
- &:hover,
- &:focus {
- background: $active-background;
- color: $active-color;
- }
- }
- }
- }
-}
-
-.sidebar-top-level-items {
- margin-bottom: 60px;
-
- > li {
- > a {
- @media (min-width: $screen-sm-min) {
- margin-right: 2px;
- }
-
- &:hover {
- color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
- }
-
- &.is-showing-fly-out {
- > a {
- margin-right: 2px;
- }
-
- .sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
- position: fixed;
- top: 0;
- left: $new-sidebar-width;
- min-width: 150px;
- margin-top: -1px;
- padding: 4px 1px;
- background-color: $white-light;
- box-shadow: 2px 1px 3px $dropdown-shadow-color;
- border: 1px solid $gray-darker;
- border-left: 0;
- border-radius: 0 3px 3px 0;
-
- &::before {
- content: "";
- position: absolute;
- top: -30px;
- bottom: -30px;
- left: -10px;
- right: -30px;
- z-index: -1;
- }
-
- &.is-above {
- margin-top: 1px;
- }
-
- .divider {
- height: 1px;
- margin: 4px -1px;
- padding: 0;
- background-color: $dropdown-divider-color;
- }
-
- > .active {
- box-shadow: none;
-
- > a {
- background-color: transparent;
- }
- }
-
- a {
- padding: 8px 16px;
- color: $gl-text-color;
-
- &:hover,
- &:focus {
- background-color: $gray-darker;
- }
- }
- }
- }
- }
-
- .badge {
- background-color: $inactive-badge-background;
- color: $inactive-color;
- }
-
- &.active {
- background: $active-background;
-
- > a {
- margin-left: 4px;
- padding-left: 12px;
- }
-
- .badge {
- color: $active-color;
- font-weight: $gl-font-weight-bold;
- }
-
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
- }
- }
-
- &.active > a:hover,
- &.is-over > a {
- background-color: $white-light;
- }
- }
-}
-
-
-// Collapsed nav
-
-.toggle-sidebar-button,
-.close-nav-button {
- width: $new-sidebar-width - 2px;
- position: fixed;
- bottom: 0;
- padding: 16px;
- background-color: $gray-normal;
- border: 0;
- border-top: 2px solid $border-color;
- color: $gl-text-color-secondary;
- display: flex;
- align-items: center;
-
- i {
- font-size: 20px;
- margin-right: 8px;
- }
-
- .fa-angle-double-right {
- display: none;
- }
-
- &:hover {
- background-color: $border-color;
- color: $gl-text-color;
- }
-}
-
-.toggle-sidebar-button {
- @media (max-width: $screen-xs-max) {
- display: none;
- }
-}
-
-
-.sidebar-icons-only {
- .context-header {
- height: 61px;
-
- a {
- padding: 10px 4px;
- }
- }
-
- li a {
- padding: 12px 15px;
- }
-
- .sidebar-top-level-items > li {
- &.active a {
- padding-left: 12px;
- }
-
- .sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
- left: $new-sidebar-collapsed-width;
- }
-
- &:not(.flyout-list) {
- display: none;
- }
- }
- }
-
- .nav-icon-container {
- margin-right: 0;
- }
-
- .toggle-sidebar-button {
- width: $new-sidebar-collapsed-width - 2px;
- padding: 16px 18px;
-
- .collapse-text,
- .fa-angle-double-left {
- display: none;
- }
-
- .fa-angle-double-right {
- display: block;
- }
- }
-}
-
-.fly-out-top-item {
- > a {
- display: flex;
- }
-
- .fly-out-badge {
- margin-left: 8px;
- }
-}
-
-.fly-out-top-item-name {
- flex: 1;
-}
-
-// Mobile nav
-
-.close-nav-button {
- display: none;
-}
-
-.toggle-mobile-nav {
- display: none;
- background-color: transparent;
- border: 0;
- padding: 6px 16px;
- margin: 0 16px 0 -15px;
- height: 46px;
- border-right: 1px solid $gl-text-color-quaternary;
-
- i {
- font-size: 20px;
- color: $gl-text-color-secondary;
- }
-
- @media (max-width: $screen-xs-max) {
- display: inline-block;
- }
-}
-
-@media (max-width: $screen-xs-max) {
- .close-nav-button {
- display: flex;
- }
-}
-
-.mobile-overlay {
- display: none;
-
- &.mobile-nav-open {
- display: block;
- position: fixed;
- background-color: $black-transparent;
- height: 100%;
- width: 100%;
- z-index: 300;
- }
-}
-
-
-// Make issue boards full-height now that sub-nav is gone
-
-.boards-list {
- height: calc(100vh - #{$new-navbar-height});
-
- @media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
- height: calc(100vh - 180px);
- // scss-lint:enable DuplicateProperty
- }
-}
-
-.with-performance-bar .boards-list {
- height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
-}
-
-
-// Change color of all horizontal tabs to match the new indigo color
-.nav-links li.active a {
- border-bottom-color: $active-border;
-
- .badge {
- font-weight: $gl-font-weight-bold;
- }
-}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
new file mode 100644
index 00000000000..6c555aee20a
--- /dev/null
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -0,0 +1,6 @@
+.info-well {
+ .admin-well-statistics,
+ .admin-well-features {
+ padding-bottom: 46px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 700be173039..ca61f7a30c3 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -55,6 +55,15 @@
.boards-app {
position: relative;
+
+ @media (min-width: $screen-sm-min) {
+ transition: width $right-sidebar-transition-duration;
+ width: 100%;
+
+ &.is-compact {
+ width: calc(100% - #{$gutter_width});
+ }
+ }
}
.boards-app-loading {
@@ -78,11 +87,6 @@
height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px;
- transition: width .2s;
-
- &.is-compact {
- width: calc(100% - 290px);
- }
}
}
@@ -410,16 +414,7 @@
margin: 5px;
}
-.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
- position: absolute;
-
- &.right-sidebar {
- top: 0;
- bottom: 0;
- height: 100%;
- }
-
.issuable-sidebar-header {
position: relative;
}
@@ -457,8 +452,8 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width .2s,
- padding .2s;
+ transition: width $right-sidebar-transition-duration,
+ padding $right-sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
new file mode 100644
index 00000000000..5538e46a6c4
--- /dev/null
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -0,0 +1,9 @@
+.edit-cluster-form {
+ .clipboard-addon {
+ background-color: $white-light;
+ }
+
+ .alert-block {
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 994707422bb..ee3ca246374 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -54,12 +54,15 @@
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
- margin-right: 4px;
.stage-cell .stage-container {
margin: 3px 3px 3px 0;
}
+ .stage-container:last-child {
+ margin-right: 0;
+ }
+
.dropdown-menu {
margin-top: 11px;
}
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index 3266714396e..dfff3e15556 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -9,6 +9,14 @@
.container-image-head {
padding: 0 16px;
line-height: 4em;
+
+ .btn-link {
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+ }
}
.table.tags {
diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss
index 16702442f50..fb1899284fd 100644
--- a/app/assets/stylesheets/pages/convdev_index.scss
+++ b/app/assets/stylesheets/pages/convdev_index.scss
@@ -83,7 +83,7 @@ $space-between-cards: 8px;
border-top-color: $color-low-score;
.card-score-big {
- background-color: $red-25;
+ background-color: $red-50;
}
}
@@ -91,7 +91,7 @@ $space-between-cards: 8px;
border-top-color: $color-average-score;
.card-score-big {
- background-color: $orange-25;
+ background-color: $orange-50;
}
}
@@ -99,7 +99,7 @@ $space-between-cards: 8px;
border-top-color: $color-high-score;
.card-score-big {
- background-color: $green-25;
+ background-color: $green-50;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index a7acaf6c728..ffb5fc94475 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -77,6 +77,18 @@
word-wrap: break-word;
}
}
+
+ &.left-side-selected {
+ td.line_content.parallel.right-side {
+ @include user-select(none);
+ }
+ }
+
+ &.right-side-selected {
+ td.line_content.parallel.left-side {
+ @include user-select(none);
+ }
+ }
}
tr.line_holder.parallel {
@@ -285,6 +297,7 @@
.drag-track {
display: block;
position: absolute;
+ top: 0;
left: 12px;
height: 10px;
width: 276px;
@@ -451,7 +464,7 @@
}
.files {
- margin-top: -1px;
+ margin-top: 1px;
.diff-file:last-child {
margin-bottom: 0;
@@ -535,21 +548,23 @@
}
.diff-notes-collapse {
- position: relative;
- width: 19px;
- height: 19px;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
padding: 0;
transition: transform .1s ease-out;
z-index: 100;
+ .collapse-icon {
+ height: 50%;
+ width: 100%;
+ }
+
svg {
- position: absolute;
- left: 50%;
- top: 50%;
- margin-left: -5.5px;
- margin-top: -5.5px;
+ vertical-align: middle;
}
+ .collapse-icon,
path {
fill: $white-light;
}
@@ -586,11 +601,6 @@
top: 76px;
}
- + .files,
- + .alert {
- margin-top: 1px;
- }
-
&:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
display: none;
}
@@ -605,11 +615,6 @@
.inline-parallel-buttons {
display: none;
}
-
- + .files,
- + .alert {
- margin-top: 30px;
- }
}
}
}
@@ -634,8 +639,170 @@
padding-top: 8px;
padding-bottom: 8px;
}
+
+ .diff-changed-file {
+ display: flex;
+ align-items: center;
+ }
}
.diff-file-changes-path {
- @include str-truncated(78%);
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.note-container {
+ background-color: $gray-light;
+ border-top: 1px solid $white-normal;
+
+ // double jagged line divider
+ .discussion-notes + .discussion-notes::before,
+ .discussion-notes + .discussion-form::before {
+ content: '';
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 10px;
+ background-color: $white-light;
+ background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
+ background-position: 5px 5px,0 5px,0 5px,5px 5px;
+ background-size: 10px 10px;
+ background-repeat: repeat;
+ }
+
+ .notes {
+ position: relative;
+ }
+
+ .diff-notes-collapse {
+ position: absolute;
+ left: -12px;
+ }
+}
+
+.diff-file .note-container > .new-note,
+.note-container .discussion-notes {
+ margin-left: 100px;
+ border-left: 1px solid $white-normal;
+}
+
+.notes.active {
+ .diff-file .note-container > .new-note,
+ .note-container .discussion-notes {
+ // Override our margin and border (set for diff tab)
+ // when user is on the discussion tab for MR
+ margin-left: inherit;
+ border-left: inherit;
+ }
+}
+
+.files:not([data-can-create-note]) .frame {
+ cursor: auto;
+}
+
+.frame.click-to-comment {
+ position: relative;
+ cursor: url(icon_image_comment.svg)
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+
+ // Retina cursor
+ cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+
+ .comment-indicator {
+ position: absolute;
+ padding: 0;
+ width: (2px * $image-comment-cursor-left-offset);
+ height: (1px * $image-comment-cursor-top-offset);
+ // center the indicator to match the top left click region
+ margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
+ margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+}
+
+.frame .badge,
+.image-diff-avatar-link .badge,
+.notes > .badge {
+ position: absolute;
+ background-color: $blue-400;
+ color: $white-light;
+ border: $white-light 1px solid;
+ min-height: $gl-padding;
+ padding: 5px 8px;
+ border-radius: 12px;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.frame .badge,
+.frame .image-comment-badge {
+ // Center align badges on the frame
+ transform: translate3d(-50%, -50%, 0);
+}
+
+.image-comment-badge {
+ @include btn-comment-icon;
+ position: absolute;
+
+ &.inverted {
+ border-color: $white-light;
+ }
+}
+
+.image-diff-avatar-link {
+ position: relative;
+
+ .badge,
+ .image-comment-badge {
+ top: 25px;
+ right: 8px;
+ }
+}
+
+.notes > .badge {
+ display: none;
+ left: -13px;
+}
+
+.discussion-notes {
+ min-height: 35px;
+
+ &:first-child {
+ // First child does not have the jagged borders
+ min-height: 25px;
+ }
+
+ &.collapsed {
+ background-color: $white-light;
+
+ .diff-notes-collapse,
+ .note,
+ .discussion-reply-holder, {
+ display: none;
+ }
+
+ .notes > .badge {
+ display: block;
+ }
+ }
+}
+
+.discussion-body .image .frame {
+ position: relative;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 9362d80d4e6..3b5e411e2c5 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -207,10 +207,13 @@
}
.prometheus-state {
- margin-top: 10px;
+ max-width: 430px;
+ margin: 10px auto;
+ text-align: center;
- .state-button-section {
- margin-top: 10px;
+ .state-svg {
+ max-width: 80vw;
+ margin: 0 auto;
}
}
@@ -288,8 +291,14 @@
fill: $black;
}
- .tick > text {
- font-size: 12px;
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
+
+ > text {
+ font-size: 12px;
+ }
}
.text-metric-title {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d8a15faf7e9..dae3ec7ac42 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -5,27 +5,29 @@
margin-right: auto;
}
-.is-confidential {
+.issuable-warning-icon {
color: $orange-600;
- background-color: $orange-50;
+ background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
- margin: 0 3px 0 -4px;
+ margin: 0 $btn-side-margin 0 0;
+ width: $issuable-warning-size;
+ height: $issuable-warning-size;
+ text-align: center;
+
+ &:first-of-type {
+ margin-right: $issuable-warning-icon-margin;
+ }
}
-.is-not-confidential {
+.sidebar-item-icon {
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
-}
-
-.confidentiality {
- .is-not-confidential {
- margin: auto;
- }
- .is-confidential {
- margin: auto;
+ &.is-active {
+ color: $orange-600;
+ background-color: $orange-50;
}
}
@@ -220,17 +222,17 @@
.right-sidebar {
position: absolute;
- top: $new-navbar-height;
+ top: $header-height;
bottom: 0;
right: 0;
- transition: width .3s;
+ transition: width $right-sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
- height: calc(100% - #{$new-navbar-height});
+ height: 100%;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
@@ -449,6 +451,12 @@
}
}
}
+
+ .milestone-title span {
+ @include str-truncated(100%);
+ display: block;
+ margin: 0 4px;
+ }
}
a {
@@ -479,10 +487,10 @@
}
.with-performance-bar .right-sidebar {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height});
+ height: calc(100% - #{$header-height} - #{$performance-bar-height});
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d4dc43035eb..cf5f933a762 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -95,6 +95,8 @@
}
.omniauth-container {
+ font-size: 13px;
+
p {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 35cefd449f1..dbf3e2b763c 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
- border-top: solid 2px $green-200;
+ border-top: solid 2px $green-300;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 439636fe026..d9fb3b44d29 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -356,6 +356,10 @@
}
}
+.mr-widget-body-controls {
+ flex-wrap: wrap;
+}
+
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
@@ -645,7 +649,7 @@
}
.merge-request-tabs-holder {
- top: $new-navbar-height;
+ top: $header-height;
z-index: 200;
background-color: $white-light;
border-bottom: 1px solid $border-color;
@@ -675,7 +679,7 @@
}
.with-performance-bar .merge-request-tabs-holder {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
}
.merge-request-tabs {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 5d7c85b16ef..04b132415eb 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -101,46 +101,73 @@
}
}
-.confidential-issue-warning {
+.issuable-note-warning {
color: $orange-600;
- background-color: $orange-50;
+ background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: none;
padding: 3px 12px;
margin: auto;
align-items: center;
+
+ .icon {
+ margin-right: $issuable-warning-icon-margin;
+ }
+}
+
+.disabled-comment .issuable-note-warning {
+ border: none;
+ border-radius: $label-border-radius;
+ padding-top: $gl-vert-padding;
+ padding-bottom: $gl-vert-padding;
+
+ .icon svg {
+ position: relative;
+ top: 2px;
+ margin-right: $btn-xs-side-margin;
+ width: $gl-font-size;
+ height: $gl-font-size;
+ fill: $orange-600;
+ }
}
-.confidential-value {
+.sidebar-item-value {
.fa {
background-color: inherit;
}
}
-.confidential-warning-message {
+.sidebar-item-warning-message {
line-height: 1.5;
padding: 16px;
- .confidential-warning-message-actions {
+ .text {
+ color: $text-color;
+ }
+
+ .sidebar-item-warning-message-actions {
display: flex;
- button {
+ .btn {
flex-grow: 1;
}
}
}
-.confidential-issue-warning + .md-area {
+.issuable-note-warning + .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.discussion-form {
- padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
+.discussion-form-container {
+ padding: $gl-padding-top $gl-padding $gl-padding;
+}
+
.discussion-notes .disabled-comment {
padding: 6px 0;
}
@@ -362,7 +389,7 @@
.dropdown-menu {
top: initial;
- bottom: 40px;
+ bottom: 100%;
width: 298px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e437bad4912..96b7db3b85d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -650,29 +650,12 @@ ul.notes {
}
.add-diff-note {
+ @include btn-comment-icon;
opacity: 0;
margin-top: -2px;
- border-radius: 50%;
- background: $white-light;
- padding: 1px 5px;
- font-size: 12px;
- color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
- width: 23px;
- height: 23px;
- border: 1px solid $blue-500;
-
- &:hover {
- background: $blue-500;
- border-color: $blue-600;
- color: $white-light;
- }
-
- &:active {
- outline: 0;
- }
}
.discussion-body,
@@ -703,6 +686,12 @@ ul.notes {
color: $note-disabled-comment-color;
padding: 90px 0;
+ &.discussion-locked {
+ border: none;
+ background-color: $white-light;
+ }
+
+
a {
color: $gl-link-color;
}
@@ -727,6 +716,12 @@ ul.notes {
border-bottom-left-radius: 0;
}
+ .btn {
+ svg path {
+ fill: $gray-darkest;
+ }
+ }
+
.btn.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
@@ -741,10 +736,6 @@ ul.notes {
border: 0;
}
}
-
- .new-issue-for-discussion path {
- fill: $gray-darkest;
- }
}
}
@@ -778,6 +769,7 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
+ color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
@@ -801,7 +793,7 @@ ul.notes {
}
svg {
- fill: $gray-darkest;
+ fill: currentColor;
height: 16px;
width: 16px;
}
@@ -816,16 +808,6 @@ ul.notes {
vertical-align: middle;
}
-.discussion-next-btn {
- svg {
- margin: 0;
-
- path {
- fill: $gray-darkest;
- }
- }
-}
-
// Merge request notes in diffs
.diff-file {
// Diff is inline
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 296b6310552..8fc7a5eec9b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -209,6 +209,13 @@
}
.stage-cell {
+ &.table-section {
+ @media (min-width: $screen-md-min) {
+ min-width: 148px;
+ margin-right: -4px;
+ }
+ }
+
.mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
@@ -639,20 +646,20 @@ button.mini-pipeline-graph-dropdown-toggle {
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
- @include mini-pipeline-graph-color($green-50, $green-500, $green-600);
+ @include mini-pipeline-graph-color($green-100, $green-500, $green-600);
}
&.ci-status-icon-failed {
- @include mini-pipeline-graph-color($red-50, $red-500, $red-600);
+ @include mini-pipeline-graph-color($red-100, $red-500, $red-600);
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
- @include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600);
+ @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600);
}
&.ci-status-icon-running {
- @include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600);
+ @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600);
}
&.ci-status-icon-canceled,
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index c5d6ff66dd6..eab39f698c3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -108,6 +108,15 @@
}
}
+.subkeys-list {
+ @include basic-list;
+
+ li {
+ padding: 3px 0;
+ border: none;
+ }
+}
+
.key-list-item {
.key-list-item-info {
@media (min-width: $screen-sm-min) {
@@ -291,7 +300,7 @@ table.u2f-registrations {
.bordered-box {
border: 1px solid $blue-300;
border-radius: $border-radius-default;
- background-color: $blue-25;
+ background-color: $blue-50;
position: relative;
display: flex;
justify-content: center;
@@ -379,7 +388,7 @@ table.u2f-registrations {
.nav-wip {
border: 1px solid $blue-500;
- background: $blue-25;
+ background: $blue-50;
padding: $gl-padding;
margin-bottom: $gl-padding;
@@ -392,11 +401,11 @@ table.u2f-registrations {
}
}
-.gpg-email-badge {
+.email-badge {
display: inline;
margin-right: $gl-padding / 2;
- .gpg-email-badge-email {
+ .email-badge-email {
display: inline;
margin-right: $gl-padding / 4;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 94e4f4334d4..a086c11324d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -499,64 +499,56 @@ a.deploy-project-label {
}
}
-.fork-namespaces {
- .row {
- -webkit-flex-wrap: wrap;
- display: -webkit-flex;
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-start;
+.fork-thumbnail {
+ height: 200px;
+ width: calc((100% / 2) - #{$gl-padding * 2});
- .fork-thumbnail {
- border-radius: $border-radius-base;
- background-color: $white-light;
- border: 1px solid $border-white-light;
- height: 202px;
- margin: $gl-padding;
- text-align: center;
- width: 169px;
+ @media (min-width: $screen-md-min) {
+ width: calc((100% / 4) - #{$gl-padding * 2});
+ }
- &:hover,
- &.forked {
- background-color: $row-hover;
- border-color: $row-hover-border;
- }
+ @media (min-width: $screen-lg-min) {
+ width: calc((100% / 5) - #{$gl-padding * 2});
+ }
- .no-avatar {
- width: 100px;
- height: 100px;
- background-color: $gray-light;
- border: 1px solid $white-normal;
- margin: 0 auto;
- border-radius: 50%;
-
- i {
- font-size: 100px;
- color: $white-normal;
- }
- }
+ &:hover:not(.disabled),
+ &.forked {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
+ }
- a {
- display: block;
- width: 100%;
- height: 100%;
- padding-top: $gl-padding;
- color: $gl-text-color;
-
- .caption {
- min-height: 30px;
- padding: $gl-padding 0;
- }
- }
+ .avatar-container,
+ .identicon {
+ float: none;
+ margin-left: auto;
+ margin-right: auto;
+ }
- img {
- border-radius: 50%;
- max-width: 100px;
- }
+ a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ padding-top: $gl-padding;
+ text-decoration: none;
+
+ &.disabled {
+ opacity: .3;
+ cursor: not-allowed;
}
}
}
+.fork-thumbnail-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ > h5 {
+ width: 100%;
+ }
+}
+
.project-template,
.project-import {
.form-group {
@@ -752,7 +744,7 @@ a.deploy-project-label {
}
li.missing {
- border: 1px dashed $border-gray-normal;
+ border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
a {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 69abb13add4..c36fe25f74d 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -54,9 +54,12 @@
border-radius: $border-radius-default;
color: $almost-black;
+ .code.white pre .hll {
+ background-color: $well-light-border !important;
+ }
+
.tree-content-holder {
display: flex;
- max-height: 100vh;
min-height: 300px;
}
@@ -71,6 +74,11 @@
height: 100%;
.monaco-editor.vs {
+ .current-line {
+ border: none;
+ background: $well-light-border;
+ }
+
.line-numbers {
cursor: pointer;
@@ -84,6 +92,13 @@
}
}
+ .blob-no-preview {
+ .vertical-center {
+ justify-content: center;
+ width: 100%;
+ }
+ }
+
&.edit-mode {
.blob-viewer-container {
overflow: hidden;
@@ -103,7 +118,7 @@
overflow: auto;
> div,
- .file-content {
+ .file-content:not(.wiki) {
display: flex;
}
@@ -144,7 +159,7 @@
list-style-type: none;
background: $gray-normal;
display: inline-block;
- padding: 10px 18px;
+ padding: #{$gl-padding / 2} $gl-padding;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
@@ -168,10 +183,9 @@
a {
@include str-truncated(100px);
color: $black;
- width: 100px;
- text-align: center;
vertical-align: middle;
text-decoration: none;
+ margin-right: 12px;
&.close {
width: auto;
@@ -181,6 +195,10 @@
}
}
+ .close-icon:hover {
+ color: $hint-color;
+ }
+
.close-icon,
.unsaved-icon {
float: right;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 13dd7b5a780..db0a04a5eb3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -28,9 +28,7 @@ input[type="checkbox"]:hover {
}
.search {
- margin-right: 10px;
- margin-left: 10px;
- margin-top: ($header-height - 35) / 2;
+ margin: 4px 8px 0;
form {
@extend .form-control;
@@ -38,15 +36,24 @@ input[type="checkbox"]:hover {
padding: 4px;
width: $search-input-width;
line-height: 24px;
+ height: 32px;
+ border: 0;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration;
&:hover {
- border-color: lighten($dropdown-input-focus-border, 20%);
- box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+ box-shadow: none;
}
}
- .location-text {
- font-style: normal;
+ .location-badge {
+ height: 32px;
+ font-size: 12px;
+ margin: -4px 4px -4px -4px;
+ line-height: 25px;
+ padding: 4px 8px;
+ border-radius: $border-radius-default 0 0 $border-radius-default;
+ transition: border-color ease-in-out $default-transition-duration;
}
.search-input {
@@ -56,59 +63,36 @@ input[type="checkbox"]:hover {
margin-left: 5px;
line-height: 25px;
width: 98%;
+ color: $white-light;
+ background: none;
+ transition: color ease-in-out $default-transition-duration;
}
- .location-badge {
- line-height: 25px;
- padding: 0 5px;
- border-radius: $border-radius-default;
- font-size: 14px;
- font-style: normal;
- color: $note-disabled-comment-color;
- display: inline-block;
- background-color: $gray-normal;
- vertical-align: top;
- cursor: default;
+ .search-input::placeholder {
+ transition: color ease-in-out $default-transition-duration;
}
.search-input-container {
- display: -webkit-flex;
display: flex;
position: relative;
}
.search-input-wrap {
- // Fallback if flexbox is not supported
- display: inline-block;
- }
-
- .search-input-wrap {
- width: 100%;
-
.search-icon,
.clear-icon {
position: absolute;
right: 5px;
- top: 0;
- color: $location-icon-color;
-
- &::before {
- font-family: FontAwesome;
- font-weight: $gl-font-weight-normal;
- font-style: normal;
- }
+ top: 4px;
}
.search-icon {
- @extend .fa-search;
- transition: color 0.15s;
+ transition: color $default-transition-duration;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.clear-icon {
- @extend .fa-times;
display: none;
}
@@ -148,21 +132,32 @@ input[type="checkbox"]:hover {
form {
@extend .form-control:focus;
border-color: $dropdown-input-focus-border;
- box-shadow: 0 0 4px $search-input-focus-shadow-color;
- }
+ box-shadow: none;
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: $gl-text-color-tertiary;
+ transition: color ease-in-out $default-transition-duration;
+ }
+ }
- .location-badge {
- transition: all 0.15s;
- background-color: $location-badge-active-bg;
- color: $white-light;
- }
+ .search-input {
+ color: $gl-text-color;
+ transition: color ease-in-out $default-transition-duration;
+ }
- .search-input-wrap {
- i {
- color: $layout-link-gray;
+ .search-input::placeholder {
+ color: $gl-text-color-tertiary;
}
}
+ .location-badge {
+ transition: all $default-transition-duration;
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
+ }
+
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index fe22d186af1..a355e2dee24 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -12,3 +12,7 @@
margin-left: 10px;
}
}
+
+.registry-placeholder {
+ min-height: 60px;
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 36f622db136..25c80e1f950 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -18,7 +18,7 @@
}
&.ci-failed {
- @include status-color($red-50, $red-500, $red-600);
+ @include status-color($red-100, $red-500, $red-600);
}
&.ci-success {
@@ -39,12 +39,12 @@
&.ci-pending,
&.ci-failed_with_warnings,
&.ci-success_with_warnings {
- @include status-color($orange-50, $orange-500, $orange-700);
+ @include status-color($orange-100, $orange-500, $orange-700);
}
&.ci-info,
&.ci-running {
- @include status-color($blue-50, $blue-500, $blue-600);
+ @include status-color($blue-100, $blue-500, $blue-600);
}
&.ci-created,
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 224eee90a3f..e2f6e511c86 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -169,6 +169,14 @@
}
}
+ .tree-item-file-external-link {
+ margin-right: 4px;
+
+ span {
+ text-decoration: inherit;
+ }
+ }
+
.tree_commit {
max-width: 320px;
diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss
index 7d9f3da79c5..06733b7f1a9 100644
--- a/app/assets/stylesheets/test.scss
+++ b/app/assets/stylesheets/test.scss
@@ -15,3 +15,9 @@
-ms-animation: none !important;
animation: none !important;
}
+
+// Disable sticky changes bar for tests
+.diff-files-changed {
+ position: relative !important;
+ top: 0 !important;
+}
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index a4648b33cfa..c27f2ee3c09 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -3,9 +3,23 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
+ before_action :display_read_only_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.admin?
end
+
+ def display_read_only_information
+ return unless Gitlab::Database.read_only?
+
+ flash.now[:notice] = read_only_message
+ end
+
+ private
+
+ # Overridden in EE
+ def read_only_message
+ _('You are on a read-only GitLab instance.')
+ end
end
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 16590e66d61..fb6d8c0bb81 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -22,8 +22,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
@application = Doorkeeper::Application.new(application_params)
if @application.save
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to admin_application_url(@application)
+ redirect_to_admin_page
else
render :new
end
@@ -42,6 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end
+ protected
+
+ def redirect_to_admin_page
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+ redirect_to admin_application_url(@application)
+ end
+
private
def set_application
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index e5cba774dcb..a7ab481519d 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
- @deploy_key = deploy_keys.new(create_params.merge(user: current_user))
-
- if @deploy_key.save
+ @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute
+ if @deploy_key.persisted?
redirect_to admin_deploy_keys_path
else
render 'new'
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index cbc7a14ae83..7eb8f758807 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -29,7 +29,7 @@ class Admin::LabelsController < Admin::ApplicationController
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
- redirect_to admin_labels_path, notice: 'label was successfully updated.'
+ redirect_to admin_labels_path, notice: 'Label was successfully updated.'
else
render :edit
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index cbcef70e957..156a8e2c515 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController
end
respond_to do |format|
- result = Users::UpdateService.new(user, user_params_with_pass).execute do |user|
+ result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
user.skip_reconfirmation!
end
@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email
email = user.emails.find(params[:email_id])
- success = Emails::DestroyService.new(user, email: email.email).execute
+ success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format|
if success
@@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def update_user(&block)
- result = Users::UpdateService.new(user).execute(&block)
+ result = Users::UpdateService.new(current_user, user: user).execute(&block)
result[:status] == :success
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 97922e39ba8..967fe39256a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -25,6 +25,8 @@ class ApplicationController < ActionController::Base
around_action :set_locale
+ after_action :set_page_title_header, if: -> { request.format == :json }
+
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
@@ -83,12 +85,21 @@ class ApplicationController < ActionController::Base
super
payload[:remote_ip] = request.remote_ip
- if current_user.present?
- payload[:user_id] = current_user.id
- payload[:username] = current_user.username
+ logged_user = auth_user
+
+ if logged_user.present?
+ payload[:user_id] = logged_user.try(:id)
+ payload[:username] = logged_user.try(:username)
end
end
+ # Controllers such as GitHttpController may use alternative methods
+ # (e.g. tokens) to authenticate the user, whereas Devise sets current_user
+ def auth_user
+ return current_user if current_user.present?
+ return try(:authenticated_user)
+ end
+
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
@@ -335,4 +346,9 @@ class ApplicationController < ActionController::Base
sign_in user, store: false
end
end
+
+ def set_page_title_header
+ # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
+ response.headers['Page-Title'] = page_title('GitLab').encode('ISO-8859-1')
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index dfc8bd0ba81..10e8e54f402 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,31 +3,10 @@ class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
before_action :load_project, only: [:users]
- before_action :find_users, only: [:users]
+ before_action :load_group, only: [:users]
def users
- @users ||= User.none
- @users = @users.active
- @users = @users.reorder(:name)
- @users = @users.search(params[:search]) if params[:search].present?
- @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
- @users = @users.page(params[:page]).per(params[:per_page])
-
- if params[:todo_filter].present? && current_user
- @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
- end
-
- if params[:search].blank?
- # Include current user if available to filter by "Me"
- if params[:current_user].present? && current_user
- @users = [current_user, *@users].uniq
- end
-
- if params[:author_id].present? && current_user
- author = User.find_by_id(params[:author_id])
- @users = [author, *@users].uniq if author
- end
- end
+ @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
end
@@ -60,26 +39,14 @@ class AutocompleteController < ApplicationController
private
- def find_users
- @users =
- if @project
- user_ids = @project.team.users.pluck(:id)
-
- if params[:author_id].present?
- user_ids << params[:author_id]
- end
-
- User.where(id: user_ids)
- elsif params[:group_id].present?
+ def load_group
+ @group ||= begin
+ if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
-
- group.users
- elsif current_user
- User.all
- else
- User.none
+ group
end
+ end
end
def load_project
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 8d4ec2d6d9d..737656b3dcc 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -10,10 +10,16 @@ module Boards
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
- make_sure_position_is_set(issues)
+ make_sure_position_is_set(issues) if Gitlab::Database.read_write?
+ issues = issues.preload(:project,
+ :milestone,
+ :assignees,
+ labels: [:priorities],
+ notes: [:award_emoji, :author]
+ )
render json: {
- issues: serialize_as_json(issues.preload(:project)),
+ issues: serialize_as_json(issues),
size: issues.total_count
}
end
@@ -76,14 +82,13 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
- labels: true,
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ labels: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
- },
- user: current_user
+ }
)
end
end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index 3eb485de9db..be667687c18 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -7,11 +7,11 @@ module Ci
def create
@content = params[:content]
- @error = Ci::GitlabCiYamlProcessor.validation_message(@content)
+ @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank?
if @error.blank?
- @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
+ @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index b75e401a8df..db8c362f125 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -59,6 +59,7 @@ module AuthenticatesWithTwoFactor
sign_in(user)
else
user.increment_failed_attempts!
+ Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP")
flash.now[:alert] = 'Invalid two-factor code.'
prompt_for_two_factor(user)
end
@@ -75,6 +76,7 @@ module AuthenticatesWithTwoFactor
sign_in(user)
else
user.increment_failed_attempts!
+ Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 23909bd2d39..3181f517087 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -10,6 +10,22 @@ module IssuableCollections
private
+ def set_issues_index
+ @collection_type = "Issue"
+ @issues = issues_collection
+ @issues = @issues.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @total_pages = issues_page_count(@issues)
+
+ return if redirect_out_of_range(@issues, @total_pages)
+
+ if params[:label_name].present?
+ @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ end
+
+ @users = []
+ end
+
def issues_collection
issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
end
@@ -90,7 +106,7 @@ module IssuableCollections
# @filter_params[:authorized_only] = true
end
- @filter_params
+ @filter_params.permit(IssuableFinder::VALID_PARAMS)
end
def set_default_state
@@ -101,19 +117,32 @@ module IssuableCollections
key = 'issuable_sort'
cookies[key] = params[:sort] if params[:sort].present?
-
- # id_desc and id_asc are old values for these two.
- cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc'
- cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc'
-
+ cookies[key] = update_cookie_value(cookies[key])
params[:sort] = cookies[key]
end
def default_sort_order
case params[:state]
- when 'opened', 'all' then sort_value_recently_created
+ when 'opened', 'all' then sort_value_created_date
when 'merged', 'closed' then sort_value_recently_updated
- else sort_value_recently_created
+ else sort_value_created_date
+ end
+ end
+
+ # Update old values to the actual ones.
+ def update_cookie_value(value)
+ case value
+ when 'id_asc' then sort_value_oldest_created
+ when 'id_desc' then sort_value_recently_created
+ when 'created_asc' then sort_value_created_date
+ when 'created_desc' then sort_value_created_date
+ when 'due_date_asc' then sort_value_due_date
+ when 'due_date_desc' then sort_value_due_date
+ when 'milestone_due_asc' then sort_value_milestone
+ when 'milestone_due_desc' then sort_value_milestone
+ when 'downvotes_asc' then sort_value_popularity
+ when 'downvotes_desc' then sort_value_popularity
+ else value
end
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 18fd8eb114d..1126f706393 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -15,9 +15,9 @@ module NotesActions
notes = notes_finder.execute
.inc_relations_for_view
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] =
if noteable.discussions_rendered_on_frontend?
@@ -96,7 +96,8 @@ module NotesActions
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
- note: note.note
+ note: note.note,
+ on_image: note.try(:on_image?)
)
discussion = note.to_discussion(noteable)
@@ -122,7 +123,9 @@ module NotesActions
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
- if params[:view] == 'parallel'
+ on_image = discussion.on_image?
+
+ if params[:view] == 'parallel' && !on_image
template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
@@ -132,7 +135,9 @@ module NotesActions
end
else
template = "discussions/_diff_discussion"
- locals = { discussions: [discussion] }
+ @fresh_discussion = true
+
+ locals = { discussions: [discussion], on_image: on_image }
end
render_to_string(
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 306afb65f10..80ab681ed87 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -10,12 +10,18 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path
end
- def after_confirmation_path_for(resource_name, resource)
- if signed_in?(resource_name)
- after_sign_in_path_for(resource)
+ def after_confirmation_path_for(_resource_name, resource)
+ # incoming resource can either be a :user or an :email
+ if signed_in?(:user)
+ after_sign_in(resource)
else
+ Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] += " Please sign in."
- new_session_path(resource_name)
+ new_session_path(:user)
end
end
+
+ def after_sign_in(resource)
+ after_sign_in_path_for(resource)
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 8057a0b455c..7ed18fb481c 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,6 +1,6 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @sort = params[:sort] || 'id_desc'
+ @sort = params[:sort] || 'created_desc'
@groups =
if params[:parent_id] && Group.supports_nested_groups?
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index f71ab702e71..cd94a36a6e7 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
- .includes(:route, :creator, namespace: :route)
+ .includes(:route, :creator, namespace: [:route, :owner])
end
def load_events
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index a8b2b93b458..02c5857eea7 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -7,9 +7,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
- if @todos.out_of_range? && @todos.total_pages != 0
- redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
- end
+
+ return if redirect_out_of_range(@todos)
end
def destroy
@@ -60,7 +59,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def find_todos
- @todos ||= TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, todo_params).execute
end
def todos_counts
@@ -69,4 +68,27 @@ class Dashboard::TodosController < Dashboard::ApplicationController
done_count: number_with_delimiter(current_user.todos_done_count)
}
end
+
+ def todo_params
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
+ end
+
+ def redirect_out_of_range(todos)
+ total_pages =
+ if todo_params.except(:sort, :page).empty?
+ (current_user.todos_pending_count / todos.limit_value).ceil
+ else
+ todos.total_pages
+ end
+
+ return false if total_pages.zero?
+
+ out_of_range = todos.current_page > total_pages
+
+ if out_of_range
+ redirect_to url_for(params.merge(page: total_pages, only_path: true))
+ end
+
+ out_of_range
+ end
end
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
new file mode 100644
index 00000000000..5551057ff55
--- /dev/null
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -0,0 +1,29 @@
+module GoogleApi
+ class AuthorizationsController < ApplicationController
+ def callback
+ token, expires_at = GoogleApi::CloudPlatform::Client
+ .new(nil, callback_google_api_auth_url)
+ .get_token(params[:code])
+
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token] = token
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] =
+ expires_at.to_s
+
+ state_redirect_uri = redirect_uri_from_session_key(params[:state])
+
+ if state_redirect_uri
+ redirect_to state_redirect_uri
+ else
+ redirect_to root_path
+ end
+ end
+
+ private
+
+ def redirect_uri_from_session_key(state)
+ key = GoogleApi::CloudPlatform::Client
+ .session_key_for_redirect_uri(params[:state])
+ session[key] if key
+ end
+ end
+end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 87c0f8905ff..572915a4930 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -3,8 +3,13 @@ class HelpController < ApplicationController
layout 'help'
+ # Taken from Jekyll
+ # https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13
+ YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m
+
def index
- @help_index = File.read(Rails.root.join('doc', 'README.md'))
+ # Remove YAML frontmatter so that it doesn't look weird
+ @help_index = File.read(Rails.root.join('doc', 'README.md')).sub(YAML_FRONT_MATTER_REGEXP, '')
# Prefix Markdown links with `help/` unless they are external links
# See http://rubular.com/r/X3baHTbPO2
@@ -22,7 +27,8 @@ class HelpController < ApplicationController
path = File.join(Rails.root, 'doc', "#{@path}.md")
if File.exist?(path)
- @markdown = File.read(path)
+ # Remove YAML frontmatter so that it doesn't look weird
+ @markdown = File.read(path).gsub(YAML_FRONT_MATTER_REGEXP, '')
render 'show.html.haml'
else
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 2ae4785b12c..b02e64a132b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@application.owner = current_user
if @application.save
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to oauth_application_url(@application)
+ redirect_to_oauth_application_page
else
set_index_vars
render :index
end
end
+ protected
+
+ def redirect_to_oauth_application_page
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+ redirect_to oauth_application_url(@application)
+ end
+
private
def verify_user_oauth_applications_enabled
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 408650aac54..39b9f8a84d1 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController
def destroy
@user = current_user
- Users::UpdateService.new(@user).execute { |user| user.remove_avatar! }
+ Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
redirect_to profile_path, status: 302
end
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index ddb67d1c4d1..bbd7ba49d77 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,15 +1,14 @@
class Profiles::EmailsController < Profiles::ApplicationController
+ before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
+
def index
- @primary = current_user.email
+ @primary_email = current_user.email
@emails = current_user.emails.order_id_desc
end
def create
- @email = Emails::CreateService.new(current_user, email_params).execute
-
- if @email.errors.blank?
- NotificationService.new.new_email(@email)
- else
+ @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
+ unless @email.errors.blank?
flash[:alert] = @email.errors.full_messages.first
end
@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
def destroy
- @email = current_user.emails.find(params[:id])
-
- Emails::DestroyService.new(current_user, email: @email.email).execute
+ Emails::DestroyService.new(current_user, user: current_user).execute(@email)
respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 }
@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
end
+ def resend_confirmation_instructions
+ if Emails::ConfirmService.new(current_user, user: current_user).execute(@email)
+ flash[:notice] = "Confirmation email sent to #{@email.email}"
+ else
+ flash[:alert] = "There was a problem sending the confirmation email"
+ end
+
+ redirect_to profile_emails_url
+ end
+
private
def email_params
params.require(:email).permit(:email)
end
+
+ def find_email
+ @email = current_user.emails.find(params[:id])
+ end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 6779cc6ddac..38e3eacd229 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -2,14 +2,14 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
def index
- @gpg_keys = current_user.gpg_keys
+ @gpg_keys = current_user.gpg_keys.with_subkeys
@gpg_key = GpgKey.new
end
def create
- @gpg_key = current_user.gpg_keys.new(gpg_key_params)
+ @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute
- if @gpg_key.save
+ if @gpg_key.persisted?
redirect_to profile_gpg_keys_path
else
@gpg_keys = current_user.gpg_keys.select(&:persisted?)
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f9f0e8eef83..069e6a810f2 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = current_user.keys.new(key_params)
+ @key = Keys::CreateService.new(current_user, key_params).execute
- if @key.save
- redirect_to profile_key_path(@key)
+ if @key.persisted?
+ redirect_to_profile_key_path
else
@keys = current_user.keys.select(&:persisted?)
render :index
@@ -50,6 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
+ protected
+
+ def redirect_to_profile_key_path
+ redirect_to profile_key_path(@key)
+ end
+
private
def key_params
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 960b7512602..8a38ba65d4c 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def update
- result = Users::UpdateService.new(current_user, user_params).execute
+ result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
if result[:status] == :success
flash[:notice] = "Notification settings saved"
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 7beb52dd8e8..dcfcb855ab5 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_automatically_set: false
}
- result = Users::UpdateService.new(@user, password_attributes).execute
+ result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success
- Users::UpdateService.new(@user, password_expires_at: nil).execute
+ Users::UpdateService.new(current_user, user: @user, password_expires_at: nil).execute
redirect_to root_path, notice: 'Password successfully changed'
else
@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return
end
- result = Users::UpdateService.new(@user, password_attributes).execute
+ result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success
flash[:notice] = "Password was successfully updated. Please login with it"
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index f748d191ef4..4146deefa89 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,6 +1,7 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index
set_index_vars
+ @personal_access_token = finder.build
end
def create
@@ -38,9 +39,8 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::AVAILABLE_SCOPES
+ @scopes = Gitlab::Auth.available_scopes
- @personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index cce2a847b53..ed0f98179eb 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def update
begin
- result = Users::UpdateService.new(user, preferences_params).execute
+ result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success
flash[:notice] = 'Preferences saved.'
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 1a4f77639e7..aa9789f8a0f 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.otp_grace_period_started_at = Time.current
end
- Users::UpdateService.new(current_user).execute!
+ Users::UpdateService.new(current_user, user: current_user).execute!
if two_factor_authentication_required? && !current_user.two_factor_enabled?
two_factor_authentication_reason(
@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
- Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user|
+ Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user|
@codes = user.generate_otp_backup_codes!
end
@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def codes
- Users::UpdateService.new(current_user).execute! do |user|
+ Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes!
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index d83824fef06..5d87037f012 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController
def update
respond_to do |format|
- result = Users::UpdateService.new(@user, user_params).execute
+ result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
if result[:status] == :success
message = "Profile was successfully updated"
@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController
end
def reset_private_token
- Users::UpdateService.new(@user).execute! do |user|
+ Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_authentication_token!
end
@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController
end
def reset_incoming_email_token
- Users::UpdateService.new(@user).execute! do |user|
+ Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token!
end
@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController
end
def reset_rss_token
- Users::UpdateService.new(@user).execute! do |user|
+ Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_rss_token!
end
@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController
end
def update_username
- result = Users::UpdateService.new(@user, username: user_params[:username]).execute
+ result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute
options = if result[:status] == :success
{ notice: "Username successfully changed" }
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index a93b72a646b..3995a2fc37a 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -26,13 +26,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
blob = @entry.blob
conditionally_expand_blob(blob)
- respond_to do |format|
- format.html do
- render 'file'
- end
-
- format.json do
- render_blob_json(blob)
+ if blob.external_link?(build)
+ redirect_to blob.external_url(@project, build)
+ else
+ respond_to do |format|
+ format.html do
+ render 'file'
+ end
+
+ format.json do
+ render_blob_json(blob)
+ end
end
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 2b8f3977e6e..183a6f88a6a 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -41,6 +41,8 @@ class Projects::BlobController < Projects::ApplicationController
end
format.json do
+ page_title @blob.path, @ref, @project.name_with_namespace
+
show_json
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 747768eefb1..7f03ce07dec 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,16 +9,20 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_recently_updated
- @branches = BranchesFinder.new(@repository, params).execute
+ @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ render
end
end
format.json do
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
new file mode 100644
index 00000000000..03019b0becc
--- /dev/null
+++ b/app/controllers/projects/clusters_controller.rb
@@ -0,0 +1,136 @@
+class Projects::ClustersController < Projects::ApplicationController
+ before_action :cluster, except: [:login, :index, :new, :create]
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:new, :create]
+ before_action :authorize_google_api, only: [:new, :create]
+ before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
+
+ def index
+ if project.cluster
+ redirect_to project_cluster_path(project, project.cluster)
+ else
+ redirect_to new_project_cluster_path(project)
+ end
+ end
+
+ def login
+ begin
+ state = generate_session_key_redirect(namespace_project_clusters_url.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+ end
+
+ def new
+ @cluster = project.build_cluster
+ end
+
+ def create
+ @cluster = Ci::CreateClusterService
+ .new(project, current_user, create_params)
+ .execute(token_in_session)
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ def status
+ respond_to do |format|
+ format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 10_000)
+
+ render json: ClusterSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent_status(@cluster)
+ end
+ end
+ end
+
+ def show
+ end
+
+ def update
+ Ci::UpdateClusterService
+ .new(project, current_user, update_params)
+ .execute(cluster)
+
+ if cluster.valid?
+ flash[:notice] = "Cluster was successfully updated."
+ redirect_to project_cluster_path(project, project.cluster)
+ else
+ render :show
+ end
+ end
+
+ def destroy
+ if cluster.destroy
+ flash[:notice] = "Cluster integration was successfully removed."
+ redirect_to project_clusters_path(project), status: 302
+ else
+ flash[:notice] = "Cluster integration was not removed."
+ render :show
+ end
+ end
+
+ private
+
+ def cluster
+ @cluster ||= project.cluster.present(current_user: current_user)
+ end
+
+ def create_params
+ params.require(:cluster).permit(
+ :gcp_project_id,
+ :gcp_cluster_zone,
+ :gcp_cluster_name,
+ :gcp_cluster_size,
+ :gcp_machine_type,
+ :project_namespace,
+ :enabled)
+ end
+
+ def update_params
+ params.require(:cluster).permit(
+ :project_namespace,
+ :enabled)
+ end
+
+ def authorize_google_api
+ unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ redirect_to action: 'login'
+ end
+ end
+
+ def token_in_session
+ @token_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
+ def authorize_update_cluster!
+ access_denied! unless can?(current_user, :update_cluster, cluster)
+ end
+
+ def authorize_admin_cluster!
+ access_denied! unless can?(current_user, :admin_cluster, cluster)
+ end
+end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 1a775def506..a62f05db7db 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -20,7 +20,12 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
- format.html
+ format.html do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
+ end
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 193549663ac..3cb4eb23981 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -17,6 +17,10 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
def diff_for_path
@@ -27,7 +31,7 @@ class Projects::CompareController < Projects::ApplicationController
def create
if params[:from].blank? || params[:to].blank?
- flash[:alert] = "You must select from and to branches"
+ flash[:alert] = "You must select a Source and a Target revision"
from_to_vars = {
from: params[:from].presence,
to: params[:to].presence
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index c2e621fa190..cf8829ba95b 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKey.new(create_params.merge(user: current_user))
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 3f83bef2c79..68978f8fdd1 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -9,14 +9,12 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
- @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
+ forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
@total_forks_count = base_query.size
- @private_forks_count = @total_forks_count - @forks.size
+ @private_forks_count = @total_forks_count - forks.size
@public_forks_count = @total_forks_count - @private_forks_count
- @sort = params[:sort] || 'id_desc'
- @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
- @forks = @forks.order_by(@sort).page(params[:page])
+ @forks = forks.page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7d0e2b3e2ef..95d7a02e9e9 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
alias_method :user, :actor
+ alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ab9f132b502..b7a108a0ebd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,12 +10,13 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
+ before_action :set_issues_index, only: [:index]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:edit, :update, :move]
+ before_action :authorize_update_issue!, only: [:update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -23,20 +24,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- @collection_type = "Issue"
- @issues = issues_collection
- @issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
- @total_pages = issues_page_count(@issues)
-
- return if redirect_out_of_range(@issues, @total_pages)
-
- if params[:label_name].present?
- @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
- end
-
- @users = []
-
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
@@ -76,17 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def edit
- respond_with(@issue)
- end
-
def show
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
- @discussions = @issue.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
-
respond_to do |format|
format.html
format.json do
@@ -100,9 +80,9 @@ class Projects::IssuesController < Projects::ApplicationController
.inc_relations_for_view
.includes(:noteable)
.fresh
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- prepare_notes_for_rendering(notes)
+ notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
discussions = Discussion.build_collection(notes, @issue)
@@ -142,10 +122,6 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :edit }
- end
-
format.json do
render_issue_json
end
@@ -302,6 +278,7 @@ class Projects::IssuesController < Projects::ApplicationController
state_event
task_num
lock_version
+ discussion_locked
] + [{ label_ids: [], assignee_ids: [] }]
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 96abdac91b6..1b985ea9763 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -11,7 +11,7 @@ class Projects::JobsController < Projects::ApplicationController
def index
@scope = params[:scope]
@all_builds = project.builds.relevant
- @builds = @all_builds.order('created_at DESC')
+ @builds = @all_builds.order('ci_builds.id DESC')
@builds =
case @scope
when 'pending'
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 1b0d3aab3fa..536f908d2c5 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
skip_before_action :lfs_check_access!, only: [:deprecated]
+ before_action :lfs_check_batch_operation!, only: [:batch]
def batch
unless objects.present?
@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController
}
}
end
+
+ def lfs_check_batch_operation!
+ if upload_request? && Gitlab::Database.read_only?
+ render(
+ json: {
+ message: lfs_read_only_message
+ },
+ content_type: 'application/vnd.git-lfs+json',
+ status: 403
+ )
+ end
+ end
+
+ # Overridden in EE
+ def lfs_read_only_message
+ _('You cannot write to this read-only GitLab instance.')
+ end
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 6602b204fcb..0e71977a58a 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
- @merge_request.ensure_ref_fetched
+ @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end
def merge_request_params
@@ -34,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:target_project_id,
:task_num,
:title,
+ :discussion_locked,
label_ids: []
]
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 1096afbb798..99dc3dda9e7 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end
def selected_target_project
- if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil?
+ if @project.id.to_s == params[:target_project_id] || !@project.forked?
@project
+ elsif params[:target_project_id].present?
+ MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
+ .execute.find(params[:target_project_id])
else
- @project.forked_project_link.forked_from_project
+ @project.forked_from_project
end
end
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index d60a24d3f1d..7d16e77ef66 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -10,7 +10,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ end
end
def diff_for_path
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3aa5dadb5ca..c5204080333 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -56,6 +56,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
close_merge_request_without_source_project
check_if_can_be_merged
+ # Return if the response has already been rendered
+ return if response_body
+
respond_to do |format|
format.html do
# Build a note object for comment form
@@ -70,6 +73,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
labels
set_pipeline_variables
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
format.json do
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index dfa5e4f7f46..fb68dd771a1 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -8,19 +8,24 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
- @url = project_network_path(@project, @ref, @options.merge(format: :json))
- @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @url = project_network_path(@project, @ref, @options.merge(format: :json))
+ @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
- respond_to do |format|
- format.html do
- if @options[:extended_sha1] && !@commit
- flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+ respond_to do |format|
+ format.html do
+ if @options[:extended_sha1] && !@commit
+ flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+ end
end
- end
- format.json do
- @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ format.json do
+ @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ end
end
+
+ render
end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 41a13f6f577..ef7d047b1ad 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -66,7 +66,16 @@ class Projects::NotesController < Projects::ApplicationController
params.merge(last_fetched_at: last_fetched_at)
end
+ def authorize_admin_note!
+ return access_denied! unless can?(current_user, :admin_note, note)
+ end
+
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
+
+ def authorize_create_note!
+ return unless noteable.lockable?
+ access_denied! unless can?(current_user, :create_note, noteable)
+ end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index a3bfbf0694e..7ad7b3003af 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts
@charts = {}
- @charts[:week] = Ci::Charts::WeekChart.new(project)
- @charts[:month] = Ci::Charts::MonthChart.new(project)
- @charts[:year] = Ci::Charts::YearChart.new(project)
- @charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
+ @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
+ @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
+ @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
+ @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 1eb78d8b522..2fd015df688 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -51,13 +51,16 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
contents.push(*tree.submodules)
- @logs = contents[@offset, @limit].to_a.map do |content|
- file = @path ? File.join(@path, content.name) : content.name
- last_commit = @repo.last_commit_for_path(@commit.id, file)
- {
- file_name: content.name,
- commit: last_commit
- }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
+ @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ contents[@offset, @limit].to_a.map do |content|
+ file = @path ? File.join(@path, content.name) : content.name
+ last_commit = @repo.last_commit_for_path(@commit.id, file)
+ {
+ file_name: content.name,
+ commit: last_commit
+ }
+ end
end
offset = (@offset + @limit)
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 71e7dc70a4d..32c0fc6d14a 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -6,17 +6,26 @@ module Projects
def index
@images = project.container_repositories
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: ContainerRepositoriesSerializer
+ .new(project: project, current_user: current_user)
+ .represent(@images)
+ end
+ end
end
def destroy
if image.destroy
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Image repository has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove image repository!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index ae72bd03cfb..e602aa3f393 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
+ def index
+ respond_to do |format|
+ format.json do
+ render json: ContainerTagsSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(tags)
+ end
+ end
+ end
+
def destroy
if tag.delete
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Registry tag has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove registry tag!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
private
+ def tags
+ Kaminari::PaginatableArray.new(image.tags, limit: 15)
+ end
+
def image
@image ||= project.container_repositories
.find(params[:repository_id])
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 1fc276b8c03..f3719059f88 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -35,7 +35,12 @@ class Projects::TreeController < Projects::ApplicationController
end
format.json do
- render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
+ page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
+ end
end
end
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 6966a7c5fee..4d2fb17a19b 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -28,7 +28,7 @@ class Projects::UploadsController < Projects::ApplicationController
end
def image_or_video?
- uploader && uploader.file.exists? && uploader.image_or_video?
+ uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 968d880886c..a8ebdf5a4a9 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
- if file.on_disk?
- send_file file.on_disk_path, disposition: 'inline'
- else
- send_data(
- file.raw_data,
- type: file.mime_type,
- disposition: 'inline',
- filename: file.name
- )
- end
+ send_data(
+ file.raw_data,
+ type: file.mime_type,
+ disposition: 'inline',
+ filename: file.name
+ )
else
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index b13034d3333..a738ca9f361 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -344,6 +344,7 @@ class ProjectsController < Projects::ApplicationController
:tag_list,
:visibility_level,
:template_name,
+ :merge_method,
project_feature_attributes: %i[
builds_access_level
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 1bc6520370a..d9142311b6f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -25,27 +25,44 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
- current_user.delete_async(deleted_by: current_user)
-
- respond_to do |format|
- format.html do
- session.try(:destroy)
- redirect_to new_user_session_path, status: 302, notice: "Account scheduled for removal."
- end
+ if destroy_confirmation_valid?
+ current_user.delete_async(deleted_by: current_user)
+ session.try(:destroy)
+ redirect_to new_user_session_path, status: 303, notice: s_('Profiles|Account scheduled for removal.')
+ else
+ redirect_to profile_account_path, status: 303, alert: destroy_confirmation_failure_message
end
end
protected
+ def destroy_confirmation_valid?
+ if current_user.confirm_deletion_with_password?
+ current_user.valid_password?(params[:password])
+ else
+ current_user.username == params[:username]
+ end
+ end
+
+ def destroy_confirmation_failure_message
+ if current_user.confirm_deletion_with_password?
+ s_('Profiles|Invalid password')
+ else
+ s_('Profiles|Invalid username')
+ end
+ end
+
def build_resource(hash = nil)
super
end
def after_sign_up_path_for(user)
+ Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
user.confirmed? ? dashboard_projects_path : users_almost_there_path
end
- def after_inactive_sign_up_path_for(_resource)
+ def after_inactive_sign_up_path_for(resource)
+ Gitlab::AppLogger.info("User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:false")
users_almost_there_path
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 1b4545e4a49..19e38993038 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -13,7 +13,10 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? }
def index
- super
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ super
+ end
end
private
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index be6491d042c..c01be42c3ee 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -8,11 +8,12 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
- prepend_before_action :store_redirect_path, only: [:new]
-
+ prepend_before_action :store_redirect_uri, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
+ after_action :log_failed_login, only: [:new], if: :failed_login?
+
def new
set_minimum_password_length
@ldap_servers = Gitlab::LDAP::Config.available_servers
@@ -29,12 +30,13 @@ class SessionsController < Devise::SessionsController
end
# hide the signed-in notification
flash[:notice] = nil
- log_audit_event(current_user, with: authentication_method)
+ log_audit_event(current_user, resource, with: authentication_method)
log_user_activity(current_user)
end
end
def destroy
+ Gitlab::AppLogger.info("User Logout: username=#{current_user.username} ip=#{request.remote_ip}")
super
# hide the signed_out notice
flash[:notice] = nil
@@ -42,6 +44,14 @@ class SessionsController < Devise::SessionsController
private
+ def log_failed_login
+ Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
+ end
+
+ def failed_login?
+ (options = env["warden.options"]) && options[:action] == "unauthenticated"
+ end
+
def login_counter
@login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count')
end
@@ -55,7 +65,7 @@ class SessionsController < Devise::SessionsController
return unless user && user.require_password_creation?
- Users::UpdateService.new(user).execute do |user|
+ Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token
end
@@ -75,28 +85,36 @@ class SessionsController < Devise::SessionsController
end
end
- def store_redirect_path
- redirect_path =
+ def stored_redirect_uri
+ @redirect_to ||= stored_location_for(:redirect)
+ end
+
+ def store_redirect_uri
+ redirect_uri =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
- referer_uri = URI(request.referer)
- if referer_uri.host == Gitlab.config.gitlab.host
- referer_uri.request_uri
- else
- request.fullpath
- end
+ URI(request.referer)
else
- request.fullpath
+ URI(request.url)
end
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
- unless URI(redirect_path).path == new_user_session_path
- store_location_for(:redirect, redirect_path)
- end
+ return true if redirect_uri.path == new_user_session_path
+
+ redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
+
+ @redirect_to = redirect_to
+ store_location_for(:redirect, redirect_to)
+ end
+
+ # Overridden in EE
+ def redirect_allowed_to?(uri)
+ uri.host == Gitlab.config.gitlab.host &&
+ uri.port == Gitlab.config.gitlab.port
end
def two_factor_enabled?
- find_user.try(:two_factor_enabled?)
+ find_user&.two_factor_enabled?
end
def auto_sign_in_with_provider
@@ -123,7 +141,8 @@ class SessionsController < Devise::SessionsController
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
- def log_audit_event(user, options = {})
+ def log_audit_event(user, resource, options = {})
+ Gitlab::AppLogger.info("Successful Login: username=#{resource.username} ip=#{request.remote_ip} method=#{options[:with]} admin=#{resource.admin?}")
AuditEventService.new(user, user, options)
.for_authentication.security_event
end
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
new file mode 100644
index 00000000000..b8f52e31926
--- /dev/null
+++ b/app/finders/autocomplete_users_finder.rb
@@ -0,0 +1,60 @@
+class AutocompleteUsersFinder
+ attr_reader :current_user, :project, :group, :search, :skip_users,
+ :page, :per_page, :author_id, :params
+
+ def initialize(params:, current_user:, project:, group:)
+ @current_user = current_user
+ @project = project
+ @group = group
+ @search = params[:search]
+ @skip_users = params[:skip_users]
+ @page = params[:page]
+ @per_page = params[:per_page]
+ @author_id = params[:author_id]
+ @params = params
+ end
+
+ def execute
+ items = find_users
+ items = items.active
+ items = items.reorder(:name)
+ items = items.search(search) if search.present?
+ items = items.where.not(id: skip_users) if skip_users.present?
+ items = items.page(page).per(per_page)
+
+ if params[:todo_filter].present? && current_user
+ items = items.todo_authors(current_user.id, params[:todo_state_filter])
+ end
+
+ if search.blank?
+ # Include current user if available to filter by "Me"
+ if params[:current_user].present? && current_user
+ items = [current_user, *items].uniq
+ end
+
+ if author_id.present? && current_user
+ author = User.find_by_id(author_id)
+ items = [author, *items].uniq if author
+ end
+ end
+
+ items
+ end
+
+ private
+
+ def find_users
+ return users_from_project if project
+ return group.users if group
+ return User.all if current_user
+
+ User.none
+ end
+
+ def users_from_project
+ user_ids = project.team.users.pluck(:id)
+ user_ids << author_id if author_id.present?
+
+ User.where(id: user_ids)
+ end
+end
diff --git a/app/finders/concerns/custom_attributes_filter.rb b/app/finders/concerns/custom_attributes_filter.rb
new file mode 100644
index 00000000000..5bbf9ca242d
--- /dev/null
+++ b/app/finders/concerns/custom_attributes_filter.rb
@@ -0,0 +1,20 @@
+module CustomAttributesFilter
+ def by_custom_attributes(items)
+ return items unless params[:custom_attributes].is_a?(Hash)
+ return items unless Ability.allowed?(current_user, :read_custom_attribute)
+
+ association = items.reflect_on_association(:custom_attributes)
+ attributes_table = association.klass.arel_table
+ attributable_table = items.model.arel_table
+
+ custom_attributes = association.klass.select('true').where(
+ attributes_table[association.foreign_key]
+ .eq(attributable_table[association.association_primary_key])
+ )
+
+ # perform a subquery for each attribute to be filtered
+ params[:custom_attributes].inject(items) do |scope, (key, value)|
+ scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value))
+ end
+ end
+end
diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb
new file mode 100644
index 00000000000..28d1b31868e
--- /dev/null
+++ b/app/finders/fork_projects_finder.rb
@@ -0,0 +1,6 @@
+class ForkProjectsFinder < ProjectsFinder
+ def initialize(project, params: {}, current_user: nil)
+ project_ids = project.forks.includes(:creator).select(:id)
+ super(params: params, current_user: current_user, project_ids_relation: project_ids)
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 88d71b0a87b..0c4c4b10fb6 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder
end
def owned_groups
- current_user&.groups || Group.none
+ current_user&.owned_groups || Group.none
end
def include_public_groups?
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9848497f258..24c07f3dc70 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -25,6 +25,28 @@ class IssuableFinder
NONE = '0'.freeze
+ SCALAR_PARAMS = %i[
+ assignee_id
+ assignee_username
+ author_id
+ author_username
+ authorized_only
+ due_date
+ group_id
+ iids
+ label_name
+ milestone_title
+ non_archived
+ project_id
+ scope
+ search
+ sort
+ state
+ ].freeze
+ ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
+
+ VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze
+
attr_accessor :current_user, :params
def initialize(current_user, params = {})
@@ -244,6 +266,8 @@ class IssuableFinder
end
def by_scope(items)
+ return items.none if current_user_related? && !current_user
+
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
new file mode 100644
index 00000000000..189eb3847eb
--- /dev/null
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -0,0 +1,18 @@
+class MergeRequestTargetProjectFinder
+ attr_reader :current_user, :source_project
+
+ def initialize(current_user: nil, source_project:)
+ @current_user = current_user
+ @source_project = source_project
+ end
+
+ def execute
+ if @source_project.fork_network
+ @source_project.fork_network.projects
+ .public_or_visible_to_user(current_user)
+ .with_feature_available_for_user(:merge_requests, current_user)
+ else
+ Project.where(id: source_project)
+ end
+ end
+end
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index 33f7ae90598..1a7e97004fb 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -15,6 +15,7 @@
#
class UsersFinder
include CreatedAtFilter
+ include CustomAttributesFilter
attr_accessor :current_user, :params
@@ -32,6 +33,7 @@ class UsersFinder
users = by_external_identity(users)
users = by_external(users)
users = by_created_at(users)
+ users = by_custom_attributes(users)
users
end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index cdf5fa5d4b7..8ad94d3f723 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -30,10 +30,4 @@ module AppearancesHelper
render 'shared/logo.svg'
end
end
-
- def custom_icon(icon_name, size: 16)
- # We can't simply do the below, because there are some .erb SVGs.
- # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
- render "shared/icons/#{icon_name}.svg", size: size
- end
end
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 4ff38f86b5f..483b957decb 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -1,7 +1,29 @@
module AutoDevopsHelper
def show_auto_devops_callout?(project)
- show_callout?('auto_devops_settings_dismissed') &&
+ Feature.get(:auto_devops_banner_disabled).off? &&
+ show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) &&
- project.has_auto_devops_implicitly_disabled?
+ project.has_auto_devops_implicitly_disabled? &&
+ !project.repository.gitlab_ci_yml &&
+ !project.ci_service
+ end
+
+ def auto_devops_warning_message(project)
+ missing_domain = !project.auto_devops&.has_domain?
+ missing_service = !project.kubernetes_service&.active?
+
+ if missing_service
+ params = {
+ kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes'))
+ }
+
+ if missing_domain
+ _('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params
+ else
+ _('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params
+ end
+ elsif missing_domain
+ _('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
+ end
end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index a4c226a6aad..be11d453898 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -13,22 +13,29 @@ module AvatarsHelper
user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
- data_attributes = {}
+ data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
if has_tooltip
css_class.push('has-tooltip')
- data_attributes = { container: 'body' }
+ data_attributes[:container] = 'body'
end
- image_tag(
- avatar_url,
+ if options[:lazy]
+ css_class << 'lazy'
+ data_attributes[:src] = avatar_url
+ avatar_url = LazyImageTagHelper.placeholder_image
+ end
+
+ image_options = {
+ alt: "#{user_name}'s avatar",
+ src: avatar_url,
+ data: data_attributes,
class: css_class,
- alt: "#{user_name}'s avatar",
- title: user_name,
- data: data_attributes,
- lazy: true
- )
+ title: user_name
+ }
+
+ tag(:img, image_options)
end
def user_avatar(options = {})
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 4bd61aa8f86..7112c6ee470 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -77,4 +77,8 @@ module BoardsHelper
'max-select': dropdown_options[:data][:'max-select']
}
end
+
+ def boards_link_text
+ s_("IssueBoards|Board")
+ end
end
diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb
index ee1b7ed083e..e88fe6bcd7e 100644
--- a/app/helpers/breadcrumbs_helper.rb
+++ b/app/helpers/breadcrumbs_helper.rb
@@ -10,11 +10,7 @@ module BreadcrumbsHelper
def breadcrumb_title_link
return @breadcrumb_link if @breadcrumb_link
- if controller.available_action?(:index)
- url_for(action: "index")
- else
- request.path
- end
+ request.path
end
def breadcrumb_title(title)
@@ -25,7 +21,7 @@ module BreadcrumbsHelper
def breadcrumb_list_item(link)
content_tag "li" do
- link + icon("angle-right", class: "breadcrumbs-list-angle")
+ link + sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
end
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 85bc784d53c..aa3a9a055a0 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options
{
- title: "Build Failed ##{@build.id}",
+ title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build)
}
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 9651f9733f9..ef22cafc2e2 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -137,7 +137,7 @@ module CommitsHelper
text =
if options[:avatar]
- %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
+ content_tag(:span, person_name, class: "commit-#{options[:source]}-name")
else
person_name
end
@@ -148,9 +148,9 @@ module CommitsHelper
}
if user.nil?
- mail_to(source_email, text.html_safe, options)
+ mail_to(source_email, text, options)
else
- link_to(text.html_safe, user_path(user), options)
+ link_to(text, user_path(user), options)
end
end
@@ -176,13 +176,15 @@ module CommitsHelper
end
end
- def view_file_button(commit_sha, diff_new_path, project)
+ def view_file_button(commit_sha, diff_new_path, project, replaced: false)
+ title = replaced ? _('View replaced file @ ') : _('View file @ ')
+
link_to(
project_blob_path(project,
tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file'
) do
- raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha),
+ raw(title) + content_tag(:span, Commit.truncate_sha(commit_sha),
class: 'commit-sha')
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 28f591a4e22..4e4a66e8a02 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -33,19 +33,21 @@ module DiffHelper
end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
- content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}"
- cls = ['diff-line-num', 'unfold', 'js-unfold']
- cls << 'js-unfold-bottom' if bottom
+ content_line_class = %w[line_content match]
+ content_line_class << 'parallel' if view == :parallel
+
+ line_num_class = %w[diff-line-num unfold js-unfold]
+ line_num_class << 'js-unfold-bottom' if bottom
html = ''
if old_pos
- html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos })
- html << content unless view == :inline
+ html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
+ html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end
if new_pos
- html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos })
- html << content
+ html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
+ html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end
html.html_safe
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index b331693c789..fd88e0d794a 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,13 +1,15 @@
module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = {
- 'pushed to' => 'icon_commit',
- 'pushed new' => 'icon_commit',
- 'created' => 'icon_status_open',
- 'opened' => 'icon_status_open',
- 'closed' => 'icon_status_closed',
- 'accepted' => 'icon_code_fork',
- 'commented on' => 'icon_comment_o',
- 'deleted' => 'icon_trash_o'
+ 'pushed to' => 'commit',
+ 'pushed new' => 'commit',
+ 'created' => 'status_open',
+ 'opened' => 'status_open',
+ 'closed' => 'status_closed',
+ 'accepted' => 'fork',
+ 'commented on' => 'comment',
+ 'deleted' => 'remove',
+ 'imported' => 'import',
+ 'joined' => 'users'
}.freeze
def link_to_author(event, self_added: false)
@@ -197,7 +199,7 @@ module EventsHelper
def icon_for_event(note)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
- custom_icon(icon_name) if icon_name
+ sprite_icon(icon_name) if icon_name
end
def icon_for_profile_event(event)
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index c53ea4519da..f7e17f5cc01 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -7,7 +7,8 @@ module GraphHelper
refs << commit_refs.join(' ')
# append note count
- refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0
+ notes_count = @graph.notes[commit.id]
+ refs << "[#{notes_count} #{pluralize(notes_count, 'note')}]" if notes_count > 0
refs
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 36b79da1bde..82bceddf1f0 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -21,7 +21,7 @@ module GroupsHelper
group.ancestors.reverse.each_with_index do |parent, index|
if index > 0
- add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before)
+ add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
end
@@ -85,8 +85,8 @@ module GroupsHelper
private
- def group_title_link(group, hidable: false, show_avatar: false)
- link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do
+ def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
+ link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
@@ -125,7 +125,7 @@ module GroupsHelper
end
def default_help
- s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
+ s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.")
end
def ancestor_locked_but_you_can_override(group)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 9a404832423..ec779c1c447 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -17,6 +17,18 @@ module IconsHelper
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
+ def custom_icon(icon_name, size: 16)
+ # We can't simply do the below, because there are some .erb SVGs.
+ # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
+ render "shared/icons/#{icon_name}.svg", size: size
+ end
+
+ def sprite_icon(icon_name, size: nil, css_class: nil)
+ css_classes = size ? "s#{size}" : ""
+ css_classes << " #{css_class}" unless css_class.blank?
+ content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
+ end
+
def audit_icon(names, options = {})
case names
when "standard"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 66e1e607e01..7713fb0b9f8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -213,7 +213,6 @@ module IssuablesHelper
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
issuableRef: issuable.to_reference,
- isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
@@ -249,16 +248,25 @@ module IssuablesHelper
Gitlab::IssuablesCountForState.new(finder)[state]
end
- def close_issuable_url(issuable)
- issuable_url(issuable, close_reopen_params(issuable, :close))
+ def close_issuable_path(issuable)
+ issuable_path(issuable, close_reopen_params(issuable, :close))
end
- def reopen_issuable_url(issuable)
- issuable_url(issuable, close_reopen_params(issuable, :reopen))
+ def reopen_issuable_path(issuable)
+ issuable_path(issuable, close_reopen_params(issuable, :reopen))
end
- def close_reopen_issuable_url(issuable, should_inverse = false)
- issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
+ def close_reopen_issuable_path(issuable, should_inverse = false)
+ issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable)
+ end
+
+ def issuable_path(issuable, *options)
+ case issuable
+ when Issue
+ issue_path(issuable, *options)
+ when MergeRequest
+ merge_request_path(issuable, *options)
+ end
end
def issuable_url(issuable, *options)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index c31023f2d9a..5b2c58d193d 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -73,7 +73,8 @@ module MergeRequestsHelper
end
def target_projects(project)
- [project, project.default_merge_request_target].uniq
+ MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project)
+ .execute
end
def merge_request_button_visibility(merge_request, closed)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 446a59030a6..be8cb358de2 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -94,6 +94,12 @@ module MilestonesHelper
end
end
+ def milestone_tooltip_title(milestone)
+ if milestone.due_date
+ [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
+ end
+ end
+
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index ce028195e51..c219aa3d6a9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -130,8 +130,12 @@ module NotesHelper
end
def can_create_note?
+ issuable = @issue || @merge_request
+
if @snippet.is_a?(PersonalSnippet)
can?(current_user, :comment_personal_snippet, @snippet)
+ elsif issuable
+ can?(current_user, :create_note, issuable)
else
can?(current_user, :create_note, @project)
end
diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb
new file mode 100644
index 00000000000..45bd3606076
--- /dev/null
+++ b/app/helpers/numbers_helper.rb
@@ -0,0 +1,11 @@
+module NumbersHelper
+ def limited_counter_with_delimiter(resource, **options)
+ limit = options.fetch(:limit, 1000).to_i
+ count = resource.limit(limit + 1).count(:all)
+ if count > limit
+ number_with_delimiter(count - 1, options) + '+'
+ else
+ number_with_delimiter(count, options)
+ end
+ end
+end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 5946c475835..18b9bf214a3 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -9,7 +9,7 @@ module PageLayoutHelper
end
# Segments are seperated by middot
- @page_title.join(" \u00b7 ")
+ @page_title.join(" · ")
end
# Define or get a description for the current page
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c0114dd0256..20e050195ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -21,11 +21,14 @@ module ProjectsHelper
classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class]
- image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '')
+ avatar = avatar_icon(author, opts[:size])
+ src = opts[:lazy_load] ? nil : avatar
+
+ image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
end
def link_to_member(project, author, opts = {}, &block)
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false }
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false, lazy_load: false }
opts = default_opts.merge(opts)
return "(deleted)" unless author
@@ -137,15 +140,7 @@ module ProjectsHelper
end
def last_push_event
- return unless current_user
- return current_user.recent_push unless @project
-
- project_ids = [@project.id]
- if fork = current_user.fork_of(@project)
- project_ids << fork.id
- end
-
- current_user.recent_push(project_ids)
+ current_user&.recent_push(@project)
end
def project_feature_access_select(field)
@@ -247,8 +242,8 @@ module ProjectsHelper
end
end
- def has_projects_or_name?(projects, params)
- !!(params[:name] || any_projects?(projects))
+ def show_projects?(projects, params)
+ !!(params[:personal] || params[:name] || any_projects?(projects))
end
private
@@ -298,6 +293,7 @@ module ProjectsHelper
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
+ clusters: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -328,7 +324,7 @@ module ProjectsHelper
def git_user_name
if current_user
- current_user.name
+ current_user.name.gsub('"', '\"')
else
_("Your name")
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b408ec0c6a4..1b542ed2a96 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -1,34 +1,38 @@
module SortingHelper
def sort_options_hash
{
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_recently_updated => sort_title_recently_updated,
- sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_created_date => sort_title_created_date,
+ sort_value_downvotes => sort_title_downvotes,
+ sort_value_due_date => sort_title_due_date,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_label_priority => sort_title_label_priority,
+ sort_value_largest_group => sort_title_largest_group,
+ sort_value_largest_repo => sort_title_largest_repo,
+ sort_value_milestone => sort_title_milestone,
+ sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_milestone_soon => sort_title_milestone_soon,
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created,
- sort_value_milestone_soon => sort_title_milestone_soon,
- sort_value_milestone_later => sort_title_milestone_later,
- sort_value_due_date_soon => sort_title_due_date_soon,
- sort_value_due_date_later => sort_title_due_date_later,
- sort_value_largest_repo => sort_title_largest_repo,
- sort_value_largest_group => sort_title_largest_group,
- sort_value_recently_signin => sort_title_recently_signin,
- sort_value_oldest_signin => sort_title_oldest_signin,
- sort_value_downvotes => sort_title_downvotes,
- sort_value_upvotes => sort_title_upvotes,
- sort_value_priority => sort_title_priority,
- sort_value_label_priority => sort_title_label_priority
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_popularity => sort_title_popularity,
+ sort_value_priority => sort_title_priority,
+ sort_value_upvotes => sort_title_upvotes
}
end
def projects_sort_options_hash
options = {
- sort_value_name => sort_title_name,
- sort_value_latest_activity => sort_title_latest_activity,
- sort_value_oldest_activity => sort_title_oldest_activity,
- sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created
+ sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_name => sort_title_name,
+ sort_value_oldest_activity => sort_title_oldest_activity,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_created => sort_title_recently_created
}
if current_controller?('admin/projects')
@@ -40,160 +44,174 @@ module SortingHelper
def member_sort_options_hash
{
- sort_value_access_level_asc => sort_title_access_level_asc,
+ sort_value_access_level_asc => sort_title_access_level_asc,
sort_value_access_level_desc => sort_title_access_level_desc,
- sort_value_last_joined => sort_title_last_joined,
- sort_value_oldest_joined => sort_title_oldest_joined,
- sort_value_name => sort_title_name_asc,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_recently_signin => sort_title_recently_signin,
- sort_value_oldest_signin => sort_title_oldest_signin
+ sort_value_last_joined => sort_title_last_joined,
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_oldest_joined => sort_title_oldest_joined,
+ sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_recently_signin => sort_title_recently_signin
}
end
def milestone_sort_options_hash
{
- sort_value_name => sort_title_name_asc,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_due_date_soon => sort_title_due_date_soon,
- sort_value_due_date_later => sort_title_due_date_later,
- sort_value_start_date_soon => sort_title_start_date_soon,
- sort_value_start_date_later => sort_title_start_date_later
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_start_date_later => sort_title_start_date_later,
+ sort_value_start_date_soon => sort_title_start_date_soon
}
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
+ sort_value_name => sort_title_name,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_updated => sort_title_recently_updated
}
end
def tags_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
+ sort_value_name => sort_title_name,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_updated => sort_title_recently_updated
}
end
- def sort_title_priority
- 'Priority'
+ def sortable_item(item, path, sorted_by)
+ link_to item, path, class: sorted_by == item ? 'is-active' : ''
end
- def sort_title_label_priority
- 'Label priority'
+ # Titles.
+ def sort_title_access_level_asc
+ s_('SortOptions|Access level, ascending')
end
- def sort_title_oldest_updated
- 'Oldest updated'
+ def sort_title_access_level_desc
+ s_('SortOptions|Access level, descending')
end
- def sort_title_recently_updated
- 'Last updated'
+ def sort_title_created_date
+ s_('SortOptions|Created date')
end
- def sort_title_oldest_activity
- 'Oldest updated'
+ def sort_title_downvotes
+ s_('SortOptions|Least popular')
end
- def sort_title_latest_activity
- 'Last updated'
+ def sort_title_due_date
+ s_('SortOptions|Due date')
end
- def sort_title_oldest_created
- 'Oldest created'
+ def sort_title_due_date_later
+ s_('SortOptions|Due later')
end
- def sort_title_recently_created
- 'Last created'
+ def sort_title_due_date_soon
+ s_('SortOptions|Due soon')
end
- def sort_title_milestone_soon
- 'Milestone due soon'
+ def sort_title_label_priority
+ s_('SortOptions|Label priority')
end
- def sort_title_milestone_later
- 'Milestone due later'
+ def sort_title_largest_group
+ s_('SortOptions|Largest group')
end
- def sort_title_due_date_soon
- 'Due soon'
+ def sort_title_largest_repo
+ s_('SortOptions|Largest repository')
end
- def sort_title_due_date_later
- 'Due later'
+ def sort_title_last_joined
+ s_('SortOptions|Last joined')
end
- def sort_title_start_date_soon
- 'Start soon'
+ def sort_title_latest_activity
+ s_('SortOptions|Last updated')
end
- def sort_title_start_date_later
- 'Start later'
+ def sort_title_milestone
+ s_('SortOptions|Milestone')
+ end
+
+ def sort_title_milestone_later
+ s_('SortOptions|Milestone due later')
+ end
+
+ def sort_title_milestone_soon
+ s_('SortOptions|Milestone due soon')
end
def sort_title_name
- 'Name'
+ s_('SortOptions|Name')
end
- def sort_title_largest_repo
- 'Largest repository'
+ def sort_title_name_asc
+ s_('SortOptions|Name, ascending')
end
- def sort_title_largest_group
- 'Largest group'
+ def sort_title_name_desc
+ s_('SortOptions|Name, descending')
end
- def sort_title_recently_signin
- 'Recent sign in'
+ def sort_title_oldest_activity
+ s_('SortOptions|Oldest updated')
end
- def sort_title_oldest_signin
- 'Oldest sign in'
+ def sort_title_oldest_created
+ s_('SortOptions|Oldest created')
end
- def sort_title_downvotes
- 'Least popular'
+ def sort_title_oldest_joined
+ s_('SortOptions|Oldest joined')
end
- def sort_title_upvotes
- 'Most popular'
+ def sort_title_oldest_signin
+ s_('SortOptions|Oldest sign in')
end
- def sort_title_last_joined
- 'Last joined'
+ def sort_title_oldest_updated
+ s_('SortOptions|Oldest updated')
end
- def sort_title_oldest_joined
- 'Oldest joined'
+ def sort_title_popularity
+ s_('SortOptions|Popularity')
end
- def sort_title_access_level_asc
- 'Access level, ascending'
+ def sort_title_priority
+ s_('SortOptions|Priority')
end
- def sort_title_access_level_desc
- 'Access level, descending'
+ def sort_title_recently_created
+ s_('SortOptions|Last created')
end
- def sort_title_name_asc
- 'Name, ascending'
+ def sort_title_recently_signin
+ s_('SortOptions|Recent sign in')
end
- def sort_title_name_desc
- 'Name, descending'
+ def sort_title_recently_updated
+ s_('SortOptions|Last updated')
end
- def sort_value_last_joined
- 'last_joined'
+ def sort_title_start_date_later
+ s_('SortOptions|Start later')
end
- def sort_value_oldest_joined
- 'oldest_joined'
+ def sort_title_start_date_soon
+ s_('SortOptions|Start soon')
+ end
+
+ def sort_title_upvotes
+ s_('SortOptions|Most popular')
end
+ # Values.
def sort_value_access_level_asc
'access_level_asc'
end
@@ -202,88 +220,112 @@ module SortingHelper
'access_level_desc'
end
- def sort_value_name_desc
- 'name_desc'
+ def sort_value_created_date
+ 'created_date'
end
- def sort_value_priority
- 'priority'
+ def sort_value_downvotes
+ 'downvotes_desc'
+ end
+
+ def sort_value_due_date
+ 'due_date'
+ end
+
+ def sort_value_due_date_later
+ 'due_date_desc'
+ end
+
+ def sort_value_due_date_soon
+ 'due_date_asc'
end
def sort_value_label_priority
'label_priority'
end
- def sort_value_oldest_updated
- 'updated_asc'
+ def sort_value_largest_group
+ 'storage_size_desc'
end
- def sort_value_recently_updated
- 'updated_desc'
+ def sort_value_largest_repo
+ 'storage_size_desc'
end
- def sort_value_oldest_activity
- 'latest_activity_asc'
+ def sort_value_last_joined
+ 'last_joined'
end
def sort_value_latest_activity
'latest_activity_desc'
end
- def sort_value_oldest_created
- 'created_asc'
+ def sort_value_milestone
+ 'milestone'
end
- def sort_value_recently_created
- 'created_desc'
+ def sort_value_milestone_later
+ 'milestone_due_desc'
end
def sort_value_milestone_soon
'milestone_due_asc'
end
- def sort_value_milestone_later
- 'milestone_due_desc'
+ def sort_value_name
+ 'name_asc'
end
- def sort_value_due_date_soon
- 'due_date_asc'
+ def sort_value_name_desc
+ 'name_desc'
end
- def sort_value_due_date_later
- 'due_date_desc'
+ def sort_value_oldest_activity
+ 'latest_activity_asc'
end
- def sort_value_start_date_soon
- 'start_date_asc'
+ def sort_value_oldest_created
+ 'created_asc'
end
- def sort_value_start_date_later
- 'start_date_desc'
+ def sort_value_oldest_signin
+ 'oldest_sign_in'
end
- def sort_value_name
- 'name_asc'
+ def sort_value_oldest_joined
+ 'oldest_joined'
end
- def sort_value_largest_repo
- 'storage_size_desc'
+ def sort_value_oldest_updated
+ 'updated_asc'
end
- def sort_value_largest_group
- 'storage_size_desc'
+ def sort_value_popularity
+ 'popularity'
+ end
+
+ def sort_value_priority
+ 'priority'
+ end
+
+ def sort_value_recently_created
+ 'created_desc'
end
def sort_value_recently_signin
'recent_sign_in'
end
- def sort_value_oldest_signin
- 'oldest_sign_in'
+ def sort_value_recently_updated
+ 'updated_desc'
end
- def sort_value_downvotes
- 'downvotes_desc'
+ def sort_value_start_date_later
+ 'start_date_desc'
+ end
+
+ def sort_value_start_date_soon
+ 'start_date_asc'
end
def sort_value_upvotes
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 88f7702db1e..40d69e30188 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -87,10 +87,14 @@ module SubmoduleHelper
namespace = @project.namespace.full_path
end
- [
- namespace_project_path(namespace, base),
- namespace_project_tree_path(namespace, base, commit)
- ]
+ begin
+ [
+ namespace_project_path(namespace, base),
+ namespace_project_tree_path(namespace, base, commit)
+ ]
+ rescue ActionController::UrlGenerationError
+ [nil, nil]
+ end
end
def sanitize_submodule_url(url)
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index c98f65c7644..00fe67d6ffb 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -1,25 +1,27 @@
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
- 'commit' => 'icon_commit',
- 'description' => 'icon_edit',
- 'merge' => 'icon_merge',
- 'merged' => 'icon_merged',
- 'opened' => 'icon_status_open',
- 'closed' => 'icon_status_closed',
- 'time_tracking' => 'icon_stopwatch',
- 'assignee' => 'icon_user',
- 'title' => 'icon_edit',
- 'task' => 'icon_check_square_o',
- 'label' => 'icon_tags',
- 'cross_reference' => 'icon_random',
- 'branch' => 'icon_code_fork',
- 'confidential' => 'icon_eye_slash',
- 'visible' => 'icon_eye',
- 'milestone' => 'icon_clock_o',
- 'discussion' => 'icon_comment_o',
- 'moved' => 'icon_arrow_circle_o_right',
- 'outdated' => 'icon_edit',
- 'duplicate' => 'icon_clone'
+ 'commit' => 'commit',
+ 'description' => 'pencil',
+ 'merge' => 'git-merge',
+ 'merged' => 'git-merge',
+ 'opened' => 'issue-open',
+ 'closed' => 'issue-close',
+ 'time_tracking' => 'timer',
+ 'assignee' => 'user',
+ 'title' => 'pencil',
+ 'task' => 'task-done',
+ 'label' => 'label',
+ 'cross_reference' => 'comment-dots',
+ 'branch' => 'fork',
+ 'confidential' => 'eye-slash',
+ 'visible' => 'eye',
+ 'milestone' => 'clock',
+ 'discussion' => 'comment',
+ 'moved' => 'arrow-right',
+ 'outdated' => 'pencil',
+ 'duplicate' => 'issue-duplicate',
+ 'locked' => 'lock',
+ 'unlocked' => 'lock-open'
}.freeze
def system_note_icon_name(note)
@@ -28,7 +30,7 @@ module SystemNoteHelper
def icon_for_system_note(note)
icon_name = system_note_icon_name(note)
- custom_icon(icon_name) if icon_name
+ sprite_icon(icon_name) if icon_name
end
extend self
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 3308ab0c259..ee701076a14 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -119,8 +119,4 @@ module TabHelper
'active' if current_controller?('oauth/applications')
end
-
- def sidebar_link(href, title: nil, css: nil, &block)
- link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title }
- end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 95dbdc8ec46..c4ea0f5ac53 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -104,7 +104,7 @@ module TreeHelper
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
- return tree_join(tree.name, flatten_tree(subtree.first))
+ return tree_join(tree.name, flatten_tree(root_path, subtree.first))
else
return tree.name
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index c401030e34a..4f5edeb9bda 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -7,12 +7,6 @@ module Emails
mail(to: @user.notification_email, subject: subject("Account was created for you"))
end
- def new_email_email(email_id)
- @email = Email.find(email_id)
- @current_user = @user = @email.user
- mail(to: @user.notification_email, subject: subject("Email was added to your account"))
- end
-
def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3568e72e463..c0cc60d5ebf 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -137,11 +137,11 @@ class ApplicationSetting < ActiveRecord::Base
validates :housekeeping_full_repack_period,
presence: true,
- numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period }
+ numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period }
validates :housekeeping_gc_period,
presence: true,
- numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
+ numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period }
validates :terminal_max_session_time,
presence: true,
@@ -247,7 +247,7 @@ class ApplicationSetting < ActiveRecord::Base
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
- import_sources: Gitlab::ImportSources.values,
+ import_sources: Settings.gitlab['import_sources'],
koding_enabled: false,
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index 7267c3965d3..53bc247dec1 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -13,7 +13,7 @@ module BlobViewer
prepare!
- @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
+ @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end
def valid?
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index b35febc9ac5..8b66531ec7b 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -2,6 +2,8 @@ module Ci
class ArtifactBlob
include BlobLike
+ EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
+
attr_reader :entry
def initialize(entry)
@@ -17,6 +19,7 @@ module Ci
def size
entry.metadata[:size]
end
+ alias_method :external_size, :size
def data
"Build artifact #{path}"
@@ -30,6 +33,27 @@ module Ci
:build_artifact
end
- alias_method :external_size, :size
+ def external_url(project, job)
+ return unless external_link?(job)
+
+ components = project.full_path_components
+ components << "-/jobs/#{job.id}/artifacts/file/#{path}"
+ artifact_path = components[1..-1].join('/')
+
+ "#{pages_config.protocol}://#{components[0]}.#{pages_config.host}/#{artifact_path}"
+ end
+
+ def external_link?(job)
+ pages_config.enabled &&
+ pages_config.artifacts_server &&
+ EXTENTIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
+ job.project.public?
+ end
+
+ private
+
+ def pages_config
+ Gitlab.config.pages
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1c8364b00da..ebeab87d8fc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,7 @@ module Ci
has_many :deployments, as: :deployable
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
+ has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -230,6 +231,10 @@ module Ci
variables
end
+ def features
+ { trace_sections: true }
+ end
+
def merge_request
return @merge_request if defined?(@merge_request)
@@ -262,6 +267,10 @@ module Ci
update_attributes(coverage: coverage) if coverage.present?
end
+ def parse_trace_sections!
+ ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
+ end
+
def trace
Gitlab::Ci::Trace.new(self)
end
@@ -457,8 +466,8 @@ module Ci
return unless trace
trace = trace.dup
- Ci::MaskSecret.mask!(trace, project.runners_token) if project
- Ci::MaskSecret.mask!(trace, token)
+ Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Gitlab::Ci::MaskSecret.mask!(trace, token)
trace
end
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
new file mode 100644
index 00000000000..ccdb95546c8
--- /dev/null
+++ b/app/models/ci/build_trace_section.rb
@@ -0,0 +1,11 @@
+module Ci
+ class BuildTraceSection < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :project
+ belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
+
+ validates :section_name, :build, :project, presence: true, allow_blank: false
+ end
+end
diff --git a/app/models/ci/build_trace_section_name.rb b/app/models/ci/build_trace_section_name.rb
new file mode 100644
index 00000000000..0fdcb1ea329
--- /dev/null
+++ b/app/models/ci/build_trace_section_name.rb
@@ -0,0 +1,11 @@
+module Ci
+ class BuildTraceSectionName < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :project
+ has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id
+
+ validates :name, :project, presence: true, allow_blank: false
+ validates :name, uniqueness: { scope: :project_id }
+ end
+end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index f64bc245a67..afeae69ba39 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -1,6 +1,6 @@
module Ci
class GroupVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
include Presentable
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 871c76fbad3..cf3ce3c9e54 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,10 +1,11 @@
module Ci
class Pipeline < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasStatus
include Importable
include AfterCommitQueue
include Presentable
+ include Gitlab::OptimisticLocking
belongs_to :project
belongs_to :user
@@ -31,6 +32,7 @@ module Ci
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
delegate :id, to: :project, prefix: true
+ delegate :full_path, to: :project, prefix: true
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? }
@@ -57,6 +59,11 @@ module Ci
auto_devops_source: 2
}
+ enum failure_reason: {
+ unknown_failure: 0,
+ config_error: 1
+ }
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -108,6 +115,12 @@ module Ci
pipeline.auto_canceled_by = nil
end
+ before_transition any => :failed do |pipeline, transition|
+ transition.args.first.try do |reason|
+ pipeline.failure_reason = reason
+ end
+ end
+
after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
@@ -262,7 +275,7 @@ module Ci
end
def cancel_running
- Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
+ retry_optimistic_lock(cancelable_statuses) do |cancelable|
cancelable.find_each do |job|
yield(job) if block_given?
job.cancel
@@ -311,6 +324,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
+ def seeds_size
+ @seeds_size ||= stage_seeds.sum(&:size)
+ end
+
def has_kubernetes_active?
project.kubernetes_service&.active?
end
@@ -336,8 +353,8 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
- Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
- rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
+ Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
+ rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
rescue
@@ -402,7 +419,7 @@ module Ci
end
def update_status
- Gitlab::OptimisticLocking.retry_lock(self) do
+ retry_optimistic_lock(self) do
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
@@ -433,7 +450,7 @@ module Ci
def update_duration
return unless started_at
- self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
+ self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end
def execute_hooks
@@ -453,6 +470,10 @@ module Ci
.fabricate!
end
+ def latest_builds_with_artifacts
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index e7e02587759..10ead6b6d3b 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineSchedule < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include Importable
acts_as_paranoid
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index ee5b8733fac..af989fb14b4 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 00b419c3efa..de5aae17a15 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index b1798084787..c6509f89117 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,6 @@
module Ci
class Runner < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
@@ -174,7 +174,7 @@ module Ci
end
def assignable_for?(project)
- !locked? || projects.exists?(id: project.id)
+ is_shared? || projects.exists?(id: project.id)
end
def accepting_tags?(build)
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 5f01a0daae9..505d178ba8e 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,6 +1,6 @@
module Ci
class RunnerProject < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
belongs_to :runner
belongs_to :project
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 754c37518b3..75b8ea2a371 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,6 +1,6 @@
module Ci
class Stage < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include Importable
include HasStatus
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 6df41a3f301..b5290bcaf53 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,6 +1,6 @@
module Ci
class Trigger < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
acts_as_paranoid
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 2c860598281..215b1cf6753 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,6 +1,6 @@
module Ci
class TriggerRequest < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index cf0fe04ddaf..67d3ec81b6f 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,6 +1,6 @@
module Ci
class Variable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
include Presentable
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2ae8890c1b3..6dba154a6ea 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -25,8 +25,8 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
- # The SHA can be between 7 and 40 hex characters.
- COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
+ MIN_SHA_LENGTH = 7
+ COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field)
context = { pipeline: :single_line, project: self.project }
@@ -53,7 +53,7 @@ class Commit
# Truncate sha to 8 characters
def truncate_sha(sha)
- sha[0..7]
+ sha[0..MIN_SHA_LENGTH]
end
def max_diff_options
@@ -100,7 +100,7 @@ class Commit
def self.reference_pattern
@reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
- (?<commit>\h{7,40})
+ (?<commit>#{COMMIT_SHA_PATTERN})
}x
end
@@ -216,9 +216,8 @@ class Commit
@raw.respond_to?(method, include_private) || super
end
- # Truncate sha to 8 characters
def short_id
- @raw.short_id(7)
+ @raw.short_id(MIN_SHA_LENGTH)
end
def diff_refs
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 193e459977a..9417033d1f6 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -59,7 +59,7 @@ module CacheMarkdownField
# Update every column in a row if any one is invalidated, as we only store
# one version per row
- def refresh_markdown_cache!(do_update: false)
+ def refresh_markdown_cache
options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) }
+ end
+
+ def refresh_markdown_cache!
+ updates = refresh_markdown_cache
+
+ return unless persisted? && Gitlab::Database.read_write?
- update_columns(updates) if persisted? && do_update
+ update_columns(updates)
end
def cached_html_up_to_date?(markdown_field)
@@ -124,8 +130,8 @@ module CacheMarkdownField
end
# Using before_update here conflicts with elasticsearch-model somehow
- before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
- before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
+ before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end
class_methods do
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index eee1a36ac6b..f5cbb3becad 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -28,6 +28,10 @@ module DiscussionOnDiff
true
end
+ def file_new_path
+ first_note.position.new_path
+ end
+
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 3803e18a96e..7c3ed96bc28 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -81,6 +81,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
+ scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 265f6e48540..fc30d008dea 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -143,16 +143,18 @@ module Issuable
end
def sort(method, excluded_labels: [])
- sorted = case method.to_s
- when 'milestone_due_asc' then order_milestone_due_asc
- when 'milestone_due_desc' then order_milestone_due_desc
- when 'downvotes_desc' then order_downvotes_desc
- when 'upvotes_desc' then order_upvotes_desc
- when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
- when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
- else
- order_by(method)
- end
+ sorted =
+ case method.to_s
+ when 'downvotes_desc' then order_downvotes_desc
+ when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'milestone' then order_milestone_due_asc
+ when 'milestone_due_asc' then order_milestone_due_asc
+ when 'milestone_due_desc' then order_milestone_due_desc
+ when 'popularity' then order_upvotes_desc
+ when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
+ when 'upvotes_desc' then order_upvotes_desc
+ else order_by(method)
+ end
# Break ties with the ID column for pagination
sorted.order(id: :desc)
@@ -214,7 +216,7 @@ module Issuable
def grouping_columns(sort)
grouping_columns = [arel_table[:id]]
- if %w(milestone_due_desc milestone_due_asc).include?(sort)
+ if %w(milestone_due_desc milestone_due_asc milestone).include?(sort)
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 1c4ddabcad5..5d75b2aa6a3 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -74,4 +74,8 @@ module Noteable
def discussions_can_be_resolved_by?(user)
discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) }
end
+
+ def lockable?
+ [MergeRequest, Issue].include?(self.class)
+ end
end
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
index fed336c29d6..f6aba91bc4c 100644
--- a/app/models/concerns/repository_mirroring.rb
+++ b/app/models/concerns/repository_mirroring.rb
@@ -1,11 +1,26 @@
module RepositoryMirroring
- def set_remote_as_mirror(name)
- config = raw_repository.rugged.config
+ IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
+ IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
+ def set_remote_as_mirror(name)
# This is used to define repository as equivalent as "git clone --mirror"
- config["remote.#{name}.fetch"] = 'refs/*:refs/*'
- config["remote.#{name}.mirror"] = true
- config["remote.#{name}.prune"] = true
+ raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
+ raw_repository.rugged.config["remote.#{name}.mirror"] = true
+ raw_repository.rugged.config["remote.#{name}.prune"] = true
+ end
+
+ def set_import_remote_as_mirror(remote_name)
+ # Add first fetch with Rugged so it does not create its own.
+ raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
+
+ add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
+
+ raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
+ raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def add_remote_fetch_config(remote_name, refspec)
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
end
def fetch_mirror(remote, url)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index f5048d17d80..22fde2eb134 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -106,6 +106,10 @@ module Routable
RequestStore[full_path_key] ||= uncached_full_path
end
+ def full_path_components
+ full_path.split('/')
+ end
+
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
@full_path = nil
@@ -152,6 +156,8 @@ module Routable
end
def update_route
+ return if Gitlab::Database.read_only?
+
prepare_route
route.save
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index db3cd257584..cefa5c13c5f 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -19,14 +19,15 @@ module Sortable
module ClassMethods
def order_by(method)
case method.to_s
- when 'name_asc' then order_name_asc
- when 'name_desc' then order_name_desc
- when 'updated_asc' then order_updated_asc
- when 'updated_desc' then order_updated_desc
- when 'created_asc' then order_created_asc
+ when 'created_asc' then order_created_asc
+ when 'created_date' then order_created_desc
when 'created_desc' then order_created_desc
- when 'id_desc' then order_id_desc
- when 'id_asc' then order_id_asc
+ when 'id_asc' then order_id_asc
+ when 'id_desc' then order_id_desc
+ when 'name_asc' then order_name_asc
+ when 'name_desc' then order_name_desc
+ when 'updated_asc' then order_updated_asc
+ when 'updated_desc' then order_updated_desc
else
all
end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index a7d5de48c66..ec3543f7053 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -43,15 +43,17 @@ module TokenAuthenticatable
write_attribute(token_field, token) if token
end
+ # Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
read_attribute(token_field)
end
+ # Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
write_new_token(token_field)
- save!
+ save! if Gitlab::Database.read_write?
end
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 51768dd96bc..eae5eee4fee 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -28,10 +28,4 @@ class DeployKey < Key
def can_push_to?(project)
can_push? && has_access_to?(project)
end
-
- private
-
- # we don't want to notify the user for deploy keys
- def notify_user
- end
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 07c4846e2ac..6eba87da1a1 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
:change_position,
+ :on_text?,
+ :on_image?,
to: :first_note
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index e9a60e6ce09..d88a92dc027 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -12,8 +12,8 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
- validates :diff_line, presence: true
- validates :line_code, presence: true, line_code: true
+ validates :diff_line, presence: true, if: :on_text?
+ validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
validate :verify_supported
@@ -43,6 +43,14 @@ class DiffNote < Note
end
end
+ def on_text?
+ position.position_type == "text"
+ end
+
+ def on_image?
+ position.position_type == "image"
+ end
+
def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository)
end
@@ -56,6 +64,8 @@ class DiffNote < Note
end
def original_line_code
+ return unless on_text?
+
self.diff_file.line_code(self.diff_line)
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index b80da7b246a..437df923d2d 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -66,6 +66,10 @@ class Discussion
@context_noteable = context_noteable
end
+ def on_image?
+ false
+ end
+
def ==(other)
other.class == self.class &&
other.context_noteable == self.context_noteable &&
diff --git a/app/models/email.rb b/app/models/email.rb
index 826d4f16edb..384f38f2db7 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? }
+ scope :confirmed, -> { where.not(confirmed_at: nil) }
+
+ after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
+
+ devise :confirmable
+ self.reconfirmable = false # currently email can't be changed, no need to reconfirm
+
def email=(value)
write_attribute(:email, value.downcase.strip)
end
@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base
def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
+
+ # once email is confirmed, update the gpg signatures
+ def update_invalid_gpg_signatures
+ user.update_invalid_gpg_signatures if confirmed?
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 435eeaf0e2e..b6868ccbe8f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -7,6 +7,7 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
@@ -82,12 +83,7 @@ class Environment < ActiveRecord::Base
def set_environment_type
names = name.split('/')
- self.environment_type =
- if names.many?
- names.first
- else
- nil
- end
+ self.environment_type = names.many? ? names.first : nil
end
def includes_commit?(commit)
@@ -101,7 +97,7 @@ class Environment < ActiveRecord::Base
end
def update_merge_request_metrics?
- (environment_type || name) == "production"
+ folder_name == "production"
end
def first_deployment_for(commit)
@@ -223,6 +219,10 @@ class Environment < ActiveRecord::Base
format: :json)
end
+ def folder_name
+ self.environment_type || self.name
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/event.rb b/app/models/event.rb
index c313bbb66f8..0997b056c6a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,7 +1,7 @@
class Event < ActiveRecord::Base
include Sortable
include IgnorableColumn
- default_scope { reorder(nil).where.not(author_id: nil) }
+ default_scope { reorder(nil) }
CREATED = 1
UPDATED = 2
@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
- has_one :push_event_payload, foreign_key: :event_id
+ has_one :push_event_payload
# Callbacks
after_create :reset_project_activity
@@ -77,6 +77,12 @@ class Event < ActiveRecord::Base
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
+ # Authors are required as they're used to display who pushed data.
+ #
+ # We're just validating the presence of the ID here as foreign key constraints
+ # should ensure the ID points to a valid user.
+ validates :author_id, presence: true
+
self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
@@ -241,13 +247,7 @@ class Event < ActiveRecord::Base
def action_name
if push?
- if new_ref?
- "pushed new"
- elsif rm_ref?
- "deleted"
- else
- "pushed to"
- end
+ push_action_name
elsif closed?
"closed"
elsif merged?
@@ -263,11 +263,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
- if project.external_import?
- "imported"
- else
- "created"
- end
+ created_project_action_name
else
"opened"
end
@@ -360,6 +356,24 @@ class Event < ActiveRecord::Base
private
+ def push_action_name
+ if new_ref?
+ "pushed new"
+ elsif rm_ref?
+ "deleted"
+ else
+ "pushed to"
+ end
+ end
+
+ def created_project_action_name
+ if project.external_import?
+ "imported"
+ else
+ "created"
+ end
+ end
+
def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
new file mode 100644
index 00000000000..218e37a5312
--- /dev/null
+++ b/app/models/fork_network.rb
@@ -0,0 +1,15 @@
+class ForkNetwork < ActiveRecord::Base
+ belongs_to :root_project, class_name: 'Project'
+ has_many :fork_network_members
+ has_many :projects, through: :fork_network_members
+
+ after_create :add_root_as_member, if: :root_project
+
+ def add_root_as_member
+ projects << root_project
+ end
+
+ def find_forks_in(other_projects)
+ projects.where(id: other_projects)
+ end
+end
diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb
new file mode 100644
index 00000000000..6a9b52a1ef8
--- /dev/null
+++ b/app/models/fork_network_member.rb
@@ -0,0 +1,7 @@
+class ForkNetworkMember < ActiveRecord::Base
+ belongs_to :fork_network
+ belongs_to :project
+ belongs_to :forked_from_project, class_name: 'Project'
+
+ validates :fork_network, :project, presence: true
+end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
new file mode 100644
index 00000000000..18bd6a6dcb4
--- /dev/null
+++ b/app/models/gcp/cluster.rb
@@ -0,0 +1,113 @@
+module Gcp
+ class Cluster < ActiveRecord::Base
+ extend Gitlab::Gcp::Model
+ include Presentable
+
+ belongs_to :project, inverse_of: :cluster
+ belongs_to :user
+ belongs_to :service
+
+ default_value_for :gcp_cluster_zone, 'us-central1-a'
+ default_value_for :gcp_cluster_size, 3
+ default_value_for :gcp_machine_type, 'n1-standard-4'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :kubernetes_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :gcp_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :gcp_cluster_name,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :gcp_cluster_zone, presence: true
+
+ validates :gcp_cluster_size,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ validates :project_namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # if we do not do status transition we prevent change
+ validate :restrict_modification, on: :update, unless: :status_changed?
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |cluster|
+ cluster.gcp_token = nil
+ cluster.gcp_operation_id = nil
+ end
+
+ before_transition any => [:errored] do |cluster, transition|
+ status_reason = transition.args.first
+ cluster.status_reason = status_reason if status_reason
+ end
+ end
+
+ def project_namespace_placeholder
+ "#{project.path}-#{project.id}"
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_url
+ 'https://' + endpoint if endpoint
+ end
+
+ def restrict_modification
+ if on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 1633acd4fa9..44eda741679 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -9,6 +9,9 @@ class GpgKey < ActiveRecord::Base
belongs_to :user
has_many :gpg_signatures
+ has_many :subkeys, class_name: 'GpgKeySubkey'
+
+ scope :with_subkeys, -> { includes(:subkeys) }
validates :user, presence: true
@@ -36,11 +39,12 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create
- after_commit :notify_user, on: :create
+ after_create :generate_subkeys
def primary_keyid
super&.upcase
end
+ alias_method :keyid, :primary_keyid
def fingerprint
super&.upcase
@@ -50,6 +54,10 @@ class GpgKey < ActiveRecord::Base
super(value&.strip)
end
+ def keyids
+ [keyid].concat(subkeys.map(&:keyid))
+ end
+
def user_infos
@user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
end
@@ -74,7 +82,7 @@ class GpgKey < ActiveRecord::Base
end
def verified_and_belongs_to_email?(email)
- emails_with_verified_status.fetch(email, false)
+ emails_with_verified_status.fetch(email.downcase, false)
end
def update_invalid_gpg_signatures
@@ -83,10 +91,11 @@ class GpgKey < ActiveRecord::Base
def revoke
GpgSignature
- .where(gpg_key: self)
+ .with_key_and_subkeys(self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all(
gpg_key_id: nil,
+ gpg_key_subkey_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
@@ -108,7 +117,11 @@ class GpgKey < ActiveRecord::Base
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end
- def notify_user
- NotificationService.new.new_gpg_key(self)
+ def generate_subkeys
+ gpg_subkeys = Gitlab::Gpg.subkeys_from_key(key)
+
+ gpg_subkeys[primary_keyid]&.each do |subkey_data|
+ subkeys.create!(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
+ end
end
end
diff --git a/app/models/gpg_key_subkey.rb b/app/models/gpg_key_subkey.rb
new file mode 100644
index 00000000000..b57922aba30
--- /dev/null
+++ b/app/models/gpg_key_subkey.rb
@@ -0,0 +1,22 @@
+class GpgKeySubkey < ActiveRecord::Base
+ include ShaAttribute
+
+ sha_attribute :keyid
+ sha_attribute :fingerprint
+
+ belongs_to :gpg_key
+
+ validates :gpg_key_id, presence: true
+ validates :fingerprint, :keyid, presence: true, uniqueness: true
+
+ delegate :key, :user, :user_infos, :verified?, :verified_user_infos,
+ :verified_and_belongs_to_email?, to: :gpg_key
+
+ def keyid
+ super&.upcase
+ end
+
+ def fingerprint
+ super&.upcase
+ end
+end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 454c90d5fc4..bf88d75246f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -1,8 +1,5 @@
class GpgSignature < ActiveRecord::Base
include ShaAttribute
- include IgnorableColumn
-
- ignore_column :valid_signature
sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid
@@ -18,11 +15,42 @@ class GpgSignature < ActiveRecord::Base
belongs_to :project
belongs_to :gpg_key
+ belongs_to :gpg_key_subkey
validates :commit_sha, presence: true
validates :project_id, presence: true
validates :gpg_key_primary_keyid, presence: true
+ def self.with_key_and_subkeys(gpg_key)
+ subkey_ids = gpg_key.subkeys.pluck(:id)
+
+ where(
+ arel_table[:gpg_key_id].eq(gpg_key.id).or(
+ arel_table[:gpg_key_subkey_id].in(subkey_ids)
+ )
+ )
+ end
+
+ def gpg_key=(model)
+ case model
+ when GpgKey
+ super
+ when GpgKeySubkey
+ self.gpg_key_subkey = model
+ when NilClass
+ super
+ self.gpg_key_subkey = nil
+ end
+ end
+
+ def gpg_key
+ if gpg_key_id
+ super
+ elsif gpg_key_subkey_id
+ gpg_key_subkey
+ end
+ end
+
def gpg_key_primary_keyid
super&.upcase
end
@@ -32,6 +60,8 @@ class GpgSignature < ActiveRecord::Base
end
def gpg_commit
+ return unless commit
+
Gitlab::Gpg::Commit.new(commit)
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 8c7d492e605..155c5d972b7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -30,9 +30,6 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
- has_many :issue_assignees
- has_many :assignees, class_name: "User", through: :issue_assignees
-
validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
@@ -119,7 +116,8 @@ class Issue < ActiveRecord::Base
def self.sort(method, excluded_labels: [])
case method.to_s
- when 'due_date_asc' then order_due_date_asc
+ when 'due_date' then order_due_date_asc
+ when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
else
super
@@ -278,8 +276,6 @@ class Issue < ActiveRecord::Base
end
def update_project_counter_caches
- return unless update_project_counter_caches?
-
Projects::OpenIssuesCountService.new(project).refresh_cache
end
diff --git a/app/models/key.rb b/app/models/key.rb
index a6b4dcfec0d..f119b15c737 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -4,8 +4,6 @@ class Key < ActiveRecord::Base
include Gitlab::CurrentSettings
include Sortable
- LAST_USED_AT_REFRESH_TIME = 1.day.to_i
-
belongs_to :user
before_validation :generate_fingerprint
@@ -28,7 +26,6 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create
- after_commit :notify_user, on: :create
after_create :post_create_hook
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
@@ -37,6 +34,7 @@ class Key < ActiveRecord::Base
value&.delete!("\n\r")
value.strip! unless value.blank?
write_attribute(:key, value)
+ @public_key = nil
end
def publishable_key
@@ -55,10 +53,7 @@ class Key < ActiveRecord::Base
end
def update_last_used_at
- lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
- return unless lease.try_obtain
-
- UseKeyWorker.perform_async(id)
+ Keys::LastUsedService.new(self).execute
end
def add_to_shell
@@ -118,8 +113,4 @@ class Key < ActiveRecord::Base
"type is forbidden. Must be #{allowed_types}"
end
-
- def notify_user
- NotificationService.new.new_key(self)
- end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 958141a7358..899028a01a0 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -127,7 +127,12 @@ class Label < ActiveRecord::Base
end
def priority(project)
- priorities.find_by(project: project).try(:priority)
+ priority = if priorities.loaded?
+ priorities.first { |p| p.project == project }
+ else
+ priorities.find_by(project: project)
+ end
+ priority.try(:priority)
end
def template?
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
index 3c1d34db5fa..80fc6304fd4 100644
--- a/app/models/legacy_diff_discussion.rb
+++ b/app/models/legacy_diff_discussion.rb
@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion
true
end
+ def on_image?
+ false
+ end
+
+ def on_text?
+ true
+ end
+
def active?(*args)
return @active if @active.present?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2a56bab48a3..292122f779e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -403,7 +403,7 @@ class MergeRequest < ActiveRecord::Base
return false unless for_fork?
return true unless source_project
- !source_project.forked_from?(target_project)
+ !source_project.in_fork_network_of?(target_project)
end
def reopenable?
@@ -415,8 +415,13 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
- merge_request_diffs.create
- reload_merge_request_diff
+ fetch_ref
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ merge_request_diffs.create
+ reload_merge_request_diff
+ end
end
def reload_merge_request_diff
@@ -459,6 +464,7 @@ class MergeRequest < ActiveRecord::Base
return unless open?
old_diff_refs = self.diff_refs
+
create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
@@ -471,7 +477,7 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
- return unless unchecked?
+ return unless unchecked? && Gitlab::Database.read_write?
can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
@@ -521,6 +527,14 @@ class MergeRequest < ActiveRecord::Base
true
end
+ def ff_merge_possible?
+ project.repository.ancestor?(target_branch_sha, diff_head_sha)
+ end
+
+ def should_be_rebased?
+ project.ff_merge_must_be_possible? && !ff_merge_possible?
+ end
+
def can_cancel_merge_when_pipeline_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
@@ -549,14 +563,20 @@ class MergeRequest < ActiveRecord::Base
commits_for_notes_limit = 100
commit_ids = commit_shas.take(commits_for_notes_limit)
- Note.where(
- "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
- "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
- mr_id: id,
- commit_ids: commit_ids,
- target_project_id: target_project_id,
- source_project_id: source_project_id
- )
+ commit_notes = Note
+ .except(:order)
+ .where(project_id: [source_project_id, target_project_id])
+ .where(noteable_type: 'Commit', commit_id: commit_ids)
+
+ # We're using a UNION ALL here since this results in better performance
+ # compared to using OR statements. We're using UNION ALL since the queries
+ # used won't produce any duplicates (e.g. a note for a commit can't also be
+ # a note for an MR).
+ union = Gitlab::SQL::Union
+ .new([notes, commit_notes], remove_duplicates: false)
+ .to_sql
+
+ Note.from("(#{union}) #{Note.table_name}")
end
alias_method :discussion_notes, :related_notes
@@ -731,10 +751,9 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- has_ci_integration = source_project.try(:ci_service)
- uses_gitlab_ci = all_pipelines.any?
+ return false if has_no_commits?
- (has_ci_integration || uses_gitlab_ci) && commits.any?
+ !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end
def branch_missing?
@@ -955,8 +974,6 @@ class MergeRequest < ActiveRecord::Base
end
def update_project_counter_caches
- return unless update_project_counter_caches?
-
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 58050e1f438..faf0b95f842 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -55,7 +55,6 @@ class MergeRequestDiff < ActiveRecord::Base
end
def ensure_commit_shas
- merge_request.fetch_ref
self.start_commit_sha ||= merge_request.target_branch_sha
self.head_commit_sha ||= merge_request.source_branch_sha
self.base_commit_sha ||= find_base_sha
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index a3070a12b7c..47e6b785c39 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -162,9 +162,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :iid, full: false)
- return if group_milestone? && format != :name
-
+ def to_reference(from_project = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
@@ -241,6 +239,10 @@ class Milestone < ActiveRecord::Base
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
+ if group_milestone? && format == :iid
+ raise ArgumentError, 'Cannot refer to a group milestone by an internal id!'
+ end
+
if format == :name && !name.include?('"')
%("#{name}")
else
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4a9a23fea1f..4672881e220 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base
end
def find_fork_of(project)
- projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
+ return nil unless project.fork_network
+
+ project.fork_network.find_forks_in(projects).first
end
def lfs_enabled?
@@ -231,6 +233,13 @@ class Namespace < ActiveRecord::Base
end
def force_share_with_group_lock_on_descendants
- descendants.update_all(share_with_group_lock: true)
+ return unless Group.supports_nested_groups?
+
+ # We can't use `descendants.update_all` since Rails will throw away the WITH
+ # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
+ # different table aliases, hence we're just using WHERE IN. Since we have a
+ # maximum of 20 nested groups this should be fine.
+ Namespace.where(id: descendants.select(:id))
+ .update_all(share_with_group_lock: true)
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 3845e485413..aec7b01e23a 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -61,8 +61,11 @@ module Network
@reserved[i] = []
end
- commits_sort_by_ref.each do |commit|
- place_chain(commit)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits_sort_by_ref.each do |commit|
+ place_chain(commit)
+ end
end
# find parent spaces for not overlap lines
diff --git a/app/models/note.rb b/app/models/note.rb
index f44590e2144..ceded9f2aef 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base
Discussion.build(notes)
end
+ # Group diff discussions by line code or file path.
+ # It is not needed to group by line code when comment is
+ # on an image.
def grouped_diff_discussions(diff_refs = nil)
groups = {}
diff_notes.fresh.discussions.each do |discussion|
- line_code = discussion.line_code_in_diffs(diff_refs)
-
- if line_code
- discussions = groups[line_code] ||= []
+ group_key =
+ if discussion.on_image?
+ discussion.file_new_path
+ else
+ discussion.line_code_in_diffs(diff_refs)
+ end
+
+ if group_key
+ discussions = groups[group_key] ||= []
discussions << discussion
end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 654be927ed8..cfcb03138b7 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base
validates :scopes, presence: true
validate :validate_scopes
+ after_initialize :set_default_scopes, if: :persisted?
+
def revoke!
update!(revoked: true)
end
@@ -28,8 +30,12 @@ class PersonalAccessToken < ActiveRecord::Base
protected
def validate_scopes
- unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
+ unless revoked || scopes.all? { |scope| Gitlab::Auth.available_scopes.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
+
+ def set_default_scopes
+ self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ff5638dd155..57e91ab3b88 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
# Storage specific hooks
after_initialize :use_hashed_storage
+ after_create :check_repository_absence!
after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed?
@@ -72,6 +73,7 @@ class Project < ActiveRecord::Base
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
+ attr_accessor :skip_disk_validation
alias_attribute :title, :name
@@ -116,11 +118,20 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service
has_one :microsoft_teams_service
+ # TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project
+ # TODO: replace these relations with the fork network versions
+
+ has_one :root_of_fork_network,
+ foreign_key: 'root_project_id',
+ inverse_of: :root_project,
+ class_name: 'ForkNetwork'
+ has_one :fork_network_member
+ has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -161,8 +172,9 @@ class Project < ActiveRecord::Base
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
- has_one :project_feature
+ has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
+ has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -177,6 +189,7 @@ class Project < ActiveRecord::Base
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
@@ -190,9 +203,9 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true
- accepts_nested_attributes_for :project_feature
+ accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
- accepts_nested_attributes_for :auto_devops
+ accepts_nested_attributes_for :auto_devops, update_only: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -227,7 +240,7 @@ class Project < ActiveRecord::Base
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
- validate :can_create_repository?, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? }
+ validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -245,6 +258,9 @@ class Project < ActiveRecord::Base
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
+ scope :with_hashed_storage, -> { where('storage_version >= 1') }
+ scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
+
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
@@ -808,7 +824,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_issue_tracker
- update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
+ update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
def has_wiki?
@@ -828,7 +844,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_wiki
- update_column(:has_external_wiki, services.external_wikis.any?)
+ update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
def find_or_initialize_services(exceptions: [])
@@ -993,6 +1009,11 @@ class Project < ActiveRecord::Base
end
def forked?
+ return true if fork_network && fork_network.root_project != self
+
+ # TODO: Use only the above conditional using the `fork_network`
+ # This is the old conditional that looks at the `forked_project_link`, we
+ # fall back to this while we're migrating the new models
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
@@ -1015,24 +1036,29 @@ class Project < ActiveRecord::Base
end
# Check if repository already exists on disk
- def can_create_repository?
+ def check_repository_path_availability
+ return true if skip_disk_validation
return false unless repository_storage_path
expires_full_path_cache # we need to clear cache to validate renames correctly
- if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ # Check if repository with same path already exists on disk we can
+ # skip this for the hashed storage because the path does not change
+ if legacy_storage? && repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
return false
end
true
+ rescue GRPC::Internal # if the path is too long
+ false
end
def create_repository(force: false)
# Forked import is handled asynchronously
return if forked? && !force
- if gitlab_shell.add_repository(repository_storage_path, disk_path)
+ if gitlab_shell.add_repository(repository_storage, disk_path)
repository.after_create
true
else
@@ -1107,8 +1133,19 @@ class Project < ActiveRecord::Base
end
end
- def forked_from?(project)
- forked? && project == forked_from_project
+ def forked_from?(other_project)
+ forked? && forked_from_project == other_project
+ end
+
+ def in_fork_network_of?(other_project)
+ # TODO: Remove this in a next release when all fork_networks are populated
+ # This makes sure all MergeRequests remain valid while the projects don't
+ # have a fork_network yet.
+ return true if forked_from?(other_project)
+
+ return false if fork_network.nil? || other_project.fork_network.nil?
+
+ fork_network == other_project.fork_network
end
def origin_merge_requests
@@ -1163,6 +1200,23 @@ class Project < ActiveRecord::Base
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
+ def latest_successful_pipeline_for_default_branch
+ if defined?(@latest_successful_pipeline_for_default_branch)
+ return @latest_successful_pipeline_for_default_branch
+ end
+
+ @latest_successful_pipeline_for_default_branch =
+ pipelines.latest_successful_for(default_branch)
+ end
+
+ def latest_successful_pipeline_for(ref = nil)
+ if ref && ref != default_branch
+ pipelines.latest_successful_for(ref)
+ else
+ latest_successful_pipeline_for_default_branch
+ end
+ end
+
def enable_ci
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
@@ -1533,18 +1587,72 @@ class Project < ActiveRecord::Base
end
def legacy_storage?
- self.storage_version.nil?
+ [nil, 0].include?(self.storage_version)
+ end
+
+ def hashed_storage?
+ self.storage_version && self.storage_version >= 1
end
def renamed?
persisted? && path_changed?
end
+ def merge_method
+ if self.merge_requests_ff_only_enabled
+ :ff
+ elsif self.merge_requests_rebase_enabled
+ :rebase_merge
+ else
+ :merge
+ end
+ end
+
+ def merge_method=(method)
+ case method.to_s
+ when "ff"
+ self.merge_requests_ff_only_enabled = true
+ self.merge_requests_rebase_enabled = true
+ when "rebase_merge"
+ self.merge_requests_ff_only_enabled = false
+ self.merge_requests_rebase_enabled = true
+ when "merge"
+ self.merge_requests_ff_only_enabled = false
+ self.merge_requests_rebase_enabled = false
+ end
+ end
+
+ def ff_merge_must_be_possible?
+ self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
+ end
+
+ def migrate_to_hashed_storage!
+ return if hashed_storage?
+
+ update!(repository_read_only: true)
+
+ if repo_reference_count > 0 || wiki_reference_count > 0
+ ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
+ else
+ ProjectMigrateHashedStorageWorker.perform_async(id)
+ end
+ end
+
+ def storage_version=(value)
+ super
+
+ @storage = nil if storage_version_changed?
+ end
+
+ def gl_repository(is_wiki:)
+ Gitlab::GlRepository.gl_repository(self, is_wiki)
+ end
+
private
def storage
@storage ||=
- if self.storage_version && self.storage_version >= 1
+ if hashed_storage?
Storage::HashedProject.new(self)
else
Storage::LegacyProject.new(self)
@@ -1557,6 +1665,27 @@ class Project < ActiveRecord::Base
end
end
+ def repo_reference_count
+ Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
+ end
+
+ def wiki_reference_count
+ Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
+ end
+
+ def check_repository_absence!
+ return if skip_disk_validation
+
+ if repository_storage_path.blank? || repository_with_same_path_already_exists?
+ errors.add(:base, 'There is already a repository with that name on disk')
+ throw :abort
+ end
+ end
+
+ def repository_with_same_path_already_exists?
+ gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ end
+
# set last_activity_at to the same as created_at
def set_last_activity_at
update_column(:last_activity_at, self.created_at)
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 53731579e87..9a52edbff8e 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,8 +1,15 @@
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ def has_domain?
+ domain.present?
+ end
+
def variables
variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index fb1db0255aa..bfb8d703ec9 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
+ validates :project, presence: true
+
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index e2ad586aea7..22a65b5145e 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -3,6 +3,7 @@ require 'slack-notifier'
module ChatMessage
class BaseMessage
attr_reader :markdown
+ attr_reader :user_full_name
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
@@ -12,10 +13,19 @@ module ChatMessage
@markdown = params[:markdown] || false
@project_name = params.dig(:project, :path_with_namespace) || params[:project_name]
@project_url = params.dig(:project, :web_url) || params[:project_url]
+ @user_full_name = params.dig(:user, :name) || params[:user_full_name]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end
+ def user_combined_name
+ if user_full_name.present?
+ "#{user_full_name} (#{user_name})"
+ else
+ user_name
+ end
+ end
+
def pretext
return message if markdown
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 4b9a2b1e1f3..1327b075858 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -29,7 +29,7 @@ module ChatMessage
def activity
{
- title: "Issue #{state} by #{user_name}",
+ title: "Issue #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
@@ -40,9 +40,9 @@ module ChatMessage
def message
if state == 'opened'
- "[#{project_link}] Issue #{state} by #{user_name}"
+ "[#{project_link}] Issue #{state} by #{user_combined_name}"
else
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index 7d0de81cdf0..f412b6833d9 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -24,7 +24,7 @@ module ChatMessage
def activity
{
- title: "Merge Request #{state} by #{user_name}",
+ title: "Merge Request #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
@@ -46,7 +46,7 @@ module ChatMessage
end
def merge_request_message
- "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
+ "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
end
def merge_request_link
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index 2da4c244229..7f9486132e6 100644
--- a/app/models/project_services/chat_message/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -32,7 +32,7 @@ module ChatMessage
def activity
{
- title: "#{user_name} #{link('commented on ' + target, note_url)}",
+ title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
@@ -42,7 +42,7 @@ module ChatMessage
private
def message
- "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
+ "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title)
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index d63d4ec2b12..2135122278a 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -9,7 +9,7 @@ module ChatMessage
def initialize(data)
super
- @user_name = data.dig(:user, :name) || 'API'
+ @user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@@ -35,7 +35,7 @@ module ChatMessage
def activity
{
- title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}",
+ title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
@@ -45,7 +45,7 @@ module ChatMessage
private
def message
- "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
+ "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status} in #{pretty_duration(duration)}"
end
def humanized_status
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index c52dd6ef8ef..8d599c5f116 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -33,7 +33,7 @@ module ChatMessage
end
{
- title: "#{user_name} #{action} #{ref_type}",
+ title: "#{user_combined_name} #{action} #{ref_type}",
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
@@ -57,15 +57,15 @@ module ChatMessage
end
def new_branch_message
- "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
+ "#{user_combined_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
end
def removed_branch_message
- "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
+ "#{user_combined_name} removed #{ref_type} #{ref} from #{project_link}"
end
def push_message
- "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
+ "#{user_combined_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
end
def commit_messages
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
index a139a8ee727..d84b80f2de2 100644
--- a/app/models/project_services/chat_message/wiki_page_message.rb
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -31,7 +31,7 @@ module ChatMessage
def activity
{
- title: "#{user_name} #{action} #{wiki_page_link}",
+ title: "#{user_combined_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
@@ -41,7 +41,7 @@ module ChatMessage
private
def message
- "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
end
def description_message
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 9d37184be2c..6a3118a11b8 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -80,6 +80,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
- recipients.to_s.split(',').reject(&:blank?)
+ recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?)
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 09049824ff7..1d35426050e 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -146,7 +146,7 @@ class ProjectTeam
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
- user.authorized_project?(project, min_access_level)
+ max_member_access(user.id) >= min_access_level
end
def human_max_access(user_id)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 698fdf7a20c..bb7be29ef66 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -54,12 +54,15 @@ class ProjectWiki
[Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('')
end
- # Returns the Gollum::Wiki object.
+ # Returns the Gitlab::Git::Wiki object.
def wiki
@wiki ||= begin
- Gollum::Wiki.new(path_to_repo)
- rescue Rugged::OSError
- create_repo!
+ gl_repository = Gitlab::GlRepository.gl_repository(project, true)
+ raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
+
+ create_repo!(raw_repository) unless raw_repository.exists?
+
+ Gitlab::Git::Wiki.new(raw_repository)
end
end
@@ -86,20 +89,14 @@ class ProjectWiki
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil)
page_title, page_dir = page_title_and_dir(title)
- if page = wiki.page(page_title, version, page_dir)
+
+ if page = wiki.page(title: page_title, version: version, dir: page_dir)
WikiPage.new(self, page, true)
- else
- nil
end
end
- def find_file(name, version = nil, try_on_disk = true)
- version = wiki.ref if version.nil? # Gollum::Wiki#file ?
- if wiki_file = wiki.file(name, version, try_on_disk)
- wiki_file
- else
- nil
- end
+ def find_file(name, version = nil)
+ wiki.file(name, version)
end
def create_page(title, content, format = :markdown, message = nil)
@@ -108,7 +105,7 @@ class ProjectWiki
wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
- rescue Gollum::DuplicatePageError => e
+ rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
return false
end
@@ -116,13 +113,13 @@ class ProjectWiki
def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, title || page.name, format.to_sym, content, commit)
+ wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
update_project_activity
end
def delete_page(page, message = nil)
- wiki.delete_page(page, commit_details(:deleted, message, page.title))
+ wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity
end
@@ -145,20 +142,8 @@ class ProjectWiki
wiki.class.default_ref
end
- def create_repo!
- if init_repo(disk_path)
- wiki = Gollum::Wiki.new(path_to_repo)
- else
- raise CouldNotCreateWikiError
- end
-
- repository.after_create
-
- wiki
- end
-
def ensure_repository
- create_repo! unless repository_exists?
+ raise CouldNotCreateWikiError unless wiki.repository_exists?
end
def hook_attrs
@@ -173,24 +158,24 @@ class ProjectWiki
private
- def init_repo(disk_path)
- gitlab_shell.add_repository(project.repository_storage_path, disk_path)
+ def create_repo!(raw_repository)
+ gitlab_shell.add_repository(project.repository_storage, disk_path)
+
+ raise CouldNotCreateWikiError unless raw_repository.exists?
+
+ repository.after_create
end
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- { email: @user.email, name: @user.name, message: commit_message }
+ Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
end
def default_message(action, title)
"#{@user.username} #{action} page: #{title}"
end
- def path_to_repo
- @path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git")
- end
-
def update_project_activity
@project.touch(:last_activity_at, :last_repository_updated_at)
end
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 23ffb0d4ea8..83ce9014094 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -3,12 +3,6 @@ class PushEvent < Event
# different "action" value.
validate :validate_push_action
- # Authors are required as they're used to display who pushed data.
- #
- # We're just validating the presence of the ID here as foreign key constraints
- # should ensure the ID points to a valid user.
- validates :author_id, presence: true
-
# The project is required to build links to commits, commit ranges, etc.
#
# We're just validating the presence of the ID here as foreign key constraints
@@ -30,6 +24,44 @@ class PushEvent < Event
delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count
+ # Returns events of pushes that either pushed to an existing ref or created a
+ # new one.
+ def self.created_or_pushed
+ actions = [
+ PushEventPayload.actions[:pushed],
+ PushEventPayload.actions[:created]
+ ]
+
+ joins(:push_event_payload)
+ .where(push_event_payloads: { action: actions })
+ end
+
+ # Returns events of pushes to a branch.
+ def self.branch_events
+ ref_type = PushEventPayload.ref_types[:branch]
+
+ joins(:push_event_payload)
+ .where(push_event_payloads: { ref_type: ref_type })
+ end
+
+ # Returns PushEvent instances for which no merge requests have been created.
+ def self.without_existing_merge_requests
+ existing_mrs = MergeRequest.except(:order)
+ .select(1)
+ .where('merge_requests.source_project_id = events.project_id')
+ .where('merge_requests.source_branch = push_event_payloads.ref')
+
+ # For reasons unknown the use of #eager_load will result in the
+ # "push_event_payload" association not being set. Because of this we're
+ # using "joins" here, which does mean an additional query needs to be
+ # executed in order to retrieve the "push_event_association" when the
+ # returned PushEvent is used.
+ joins(:push_event_payload)
+ .where('NOT EXISTS (?)', existing_mrs)
+ .created_or_pushed
+ .branch_events
+ end
+
def self.sti_name
PUSHED
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 035f85a0b46..d725c65081d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -8,6 +8,7 @@ class Repository
RESERVED_REFS_NAMES = %W[
heads
tags
+ replace
#{REF_ENVIRONMENTS}
#{REF_KEEP_AROUND}
#{REF_ENVIRONMENTS}
@@ -33,7 +34,10 @@ class Repository
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
+ tag_count avatar exists? empty? root_ref has_visible_content?).freeze
+
+ # Methods that use cache_method but only memoize the value
+ MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
@@ -166,7 +170,7 @@ class Repository
end
def add_branch(user, branch_name, ref)
- branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
+ branch = raw_repository.add_branch(branch_name, user: user, target: ref)
after_create_branch
@@ -176,7 +180,7 @@ class Repository
end
def add_tag(user, tag_name, target, message = nil)
- raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
+ raw_repository.add_tag(tag_name, user: user, target: target, message: message)
rescue Gitlab::Git::Repository::InvalidRef
false
end
@@ -184,7 +188,7 @@ class Repository
def rm_branch(user, branch_name)
before_remove_branch
- raw_repository.rm_branch(branch_name, committer: user)
+ raw_repository.rm_branch(branch_name, user: user)
after_remove_branch
true
@@ -193,7 +197,7 @@ class Repository
def rm_tag(user, tag_name)
before_remove_tag
- raw_repository.rm_tag(tag_name, committer: user)
+ raw_repository.rm_tag(tag_name, user: user)
after_remove_tag
true
@@ -268,7 +272,7 @@ class Repository
end
def expire_branches_cache
- expire_method_caches(%i(branch_names branch_count))
+ expire_method_caches(%i(branch_names branch_count has_visible_content?))
@local_branches = nil
@branch_exists_memo = nil
end
@@ -339,7 +343,7 @@ class Repository
def expire_emptiness_caches
return unless empty?
- expire_method_caches(%i(empty?))
+ expire_method_caches(%i(empty? has_visible_content?))
end
def lookup_cache
@@ -482,13 +486,7 @@ class Repository
def exists?
return false unless full_path
- Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
- if enabled
- raw_repository.exists?
- else
- refs_directory_exists?
- end
- end
+ raw_repository.exists?
end
cache_method :exists?
@@ -522,13 +520,17 @@ class Repository
delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
- delegate :branch_count, :tag_count, to: :raw_repository
+ delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0
cache_method :tag_count, fallback: 0
+ cache_method :has_visible_content?, fallback: false
def avatar
- if tree = file_on_head(:avatar)
- tree.path
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ if tree = file_on_head(:avatar)
+ tree.path
+ end
end
end
cache_method :avatar
@@ -762,17 +764,23 @@ class Repository
multi_action(**options)
end
- def with_branch(user, *args)
- result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
- yield start_commit
- end
+ def with_cache_hooks
+ result = yield
- newrev, should_run_after_create, should_run_after_create_branch = result
+ return unless result
- after_create if should_run_after_create
- after_create_branch if should_run_after_create_branch
+ after_create if result.repo_created?
+ after_create_branch if result.branch_created?
- newrev
+ result.newrev
+ end
+
+ def with_branch(user, *args)
+ with_cache_hooks do
+ Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
+ yield start_commit
+ end
+ end
end
# rubocop:disable Metrics/ParameterLists
@@ -822,10 +830,6 @@ class Repository
}
end
- def user_to_committer(user)
- Gitlab::Git.committer_hash(email: user.email, name: user.name)
- end
-
def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
@@ -837,81 +841,63 @@ class Repository
end
end
- def merge(user, source, merge_request, options = {})
- with_branch(
- user,
- merge_request.target_branch) do |start_commit|
- our_commit = start_commit.sha
- their_commit = source
+ def merge(user, source_sha, merge_request, message)
+ with_cache_hooks do
+ raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
+ merge_request.update(in_progress_merge_commit_sha: commit_id)
+ nil # Return value does not matter.
+ end
+ end
+ end
- raise 'Invalid merge target' unless our_commit
- raise 'Invalid merge source' unless their_commit
+ def ff_merge(user, source, target_branch, merge_request: nil)
+ our_commit = rugged.branches[target_branch].target
+ their_commit =
+ if source.is_a?(Gitlab::Git::Commit)
+ source.raw_commit
+ else
+ rugged.lookup(source)
+ end
- merge_index = rugged.merge_commits(our_commit, their_commit)
- break if merge_index.conflicts?
+ raise 'Invalid merge target' if our_commit.nil?
+ raise 'Invalid merge source' if their_commit.nil?
- actual_options = options.merge(
- parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged)
- )
+ with_branch(user, target_branch) do |start_commit|
+ merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
- commit_id = create_commit(actual_options)
- merge_request.update(in_progress_merge_commit_sha: commit_id)
- commit_id
+ their_commit.oid
end
- rescue Gitlab::Git::CommitError # when merge_index.conflicts?
- false
end
def revert(
- user, commit, branch_name,
+ user, commit, branch_name, message,
start_branch_name: nil, start_project: project)
- with_branch(
- user,
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository) do |start_commit|
-
- revert_tree_id = check_revert_content(commit, start_commit.sha)
- unless revert_tree_id
- raise Repository::CreateTreeError.new('Failed to revert commit')
- end
-
- committer = user_to_committer(user)
- create_commit(message: commit.revert_message(user),
- author: committer,
- committer: committer,
- tree: revert_tree_id,
- parents: [start_commit.sha])
+ with_cache_hooks do
+ raw_repository.revert(
+ user: user,
+ commit: commit.raw,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_project.repository.raw_repository
+ )
end
end
def cherry_pick(
- user, commit, branch_name,
+ user, commit, branch_name, message,
start_branch_name: nil, start_project: project)
- with_branch(
- user,
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository) do |start_commit|
-
- cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
- unless cherry_pick_tree_id
- raise Repository::CreateTreeError.new('Failed to cherry-pick commit')
- end
- committer = user_to_committer(user)
-
- create_commit(message: commit.cherry_pick_message(user),
- author: {
- email: commit.author_email,
- name: commit.author_name,
- time: commit.authored_date
- },
- committer: committer,
- tree: cherry_pick_tree_id,
- parents: [start_commit.sha])
+ with_cache_hooks do
+ raw_repository.cherry_pick(
+ user: user,
+ commit: commit.raw,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_project.repository.raw_repository
+ )
end
end
@@ -923,36 +909,6 @@ class Repository
end
end
- def check_revert_content(target_commit, source_sha)
- args = [target_commit.sha, source_sha]
- args << { mainline: 1 } if target_commit.merge_commit?
-
- revert_index = rugged.revert_commit(*args)
- return false if revert_index.conflicts?
-
- tree_id = revert_index.write_tree(rugged)
- return false unless diff_exists?(source_sha, tree_id)
-
- tree_id
- end
-
- def check_cherry_pick_content(target_commit, source_sha)
- args = [target_commit.sha, source_sha]
- args << 1 if target_commit.merge_commit?
-
- cherry_pick_index = rugged.cherrypick_commit(*args)
- return false if cherry_pick_index.conflicts?
-
- tree_id = cherry_pick_index.write_tree(rugged)
- return false unless diff_exists?(source_sha, tree_id)
-
- tree_id
- end
-
- def diff_exists?(sha1, sha2)
- rugged.diff(sha1, sha2).size > 0
- end
-
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
@@ -988,6 +944,7 @@ class Repository
def empty_repo?
!exists? || !has_visible_content?
end
+ cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
@@ -1032,7 +989,7 @@ class Repository
end
def create_ref(ref, ref_path)
- fetch_ref(path_to_repo, ref, ref_path)
+ raw_repository.write_ref(ref_path, ref)
end
def ls_files(ref)
@@ -1117,12 +1074,6 @@ class Repository
blob.data
end
- def refs_directory_exists?
- circuit_breaker.perform do
- File.exist?(File.join(path_to_repo, 'refs'))
- end
- end
-
def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id)
@@ -1151,12 +1102,6 @@ class Repository
Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags))
end
- def create_commit(params = {})
- params[:message].delete!("\r")
-
- Rugged::Commit.create(rugged, params)
- end
-
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c)
@@ -1180,10 +1125,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
end
- def circuit_breaker
- @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
- end
-
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 298569cb7a6..6e311806be1 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -53,13 +53,17 @@ class SentNotification < ActiveRecord::Base
end
def unsubscribable?
- !for_commit?
+ !(for_commit? || for_snippet?)
end
def for_commit?
noteable_type == "Commit"
end
+ def for_snippet?
+ noteable_type.end_with?('Snippet')
+ end
+
def noteable
if for_commit?
project.commit(commit_id) rescue nil
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index fae1b64961a..f025f40994e 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -4,6 +4,7 @@ module Storage
delegate :gitlab_shell, :repository_storage_path, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze
+ STORAGE_VERSION = 1
def initialize(project)
@project = project
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 0b33e45473b..1f9f8d7286b 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
- opened closed merged duplicate
+ opened closed merged duplicate locked unlocked
outdated
].freeze
diff --git a/app/models/user.rb b/app/models/user.rb
index d7549409b15..533a776bc65 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -60,7 +60,7 @@ class User < ActiveRecord::Base
lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
- Users::UpdateService.new(self).execute(validate: false)
+ Users::UpdateService.new(self, user: self).execute(validate: false)
end
attr_accessor :force_random_password
@@ -130,6 +130,8 @@ class User < ActiveRecord::Base
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
+ has_many :custom_attributes, class_name: 'UserCustomAttribute'
+
#
# Validations
#
@@ -161,15 +163,16 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
-
- after_update :update_emails_with_primary_email, if: :email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
+ before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
+ after_destroy :post_destroy_hook
+ after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
+
after_initialize :set_projects_limit
- after_destroy :post_destroy_hook
# User's Layout preference
enum layout: [:fixed, :fluid]
@@ -456,6 +459,14 @@ class User < ActiveRecord::Base
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
+ def remember_me!
+ super if ::Gitlab::Database.read_write?
+ end
+
+ def forget_me!
+ super if ::Gitlab::Database.read_write?
+ end
+
def disable_two_factor!
transaction do
update_attributes(
@@ -523,12 +534,24 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
+ # see if the new email is already a verified secondary email
+ def check_for_verified_email
+ skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
+ end
+
+ # Note: the use of the Emails services will cause `saves` on the user object, running
+ # through the callbacks again and can have side effects, such as the `previous_changes`
+ # hash and `_was` variables getting munged.
+ # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
+ # scenario, though it then requires us to use the `previous_changes` hash
def update_emails_with_primary_email
+ previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
primary_email_record = emails.find_by(email: email)
- if primary_email_record
- Emails::DestroyService.new(self, email: email).execute
- Emails::CreateService.new(self, email: email_was).execute
- end
+ Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
+
+ # the original primary email was confirmed, and we want that to carry over. We don't
+ # have access to the original confirmation values at this point, so just set confirmed_at
+ Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
end
def update_invalid_gpg_signatures
@@ -639,6 +662,10 @@ class User < ActiveRecord::Base
Ability.allowed?(self, action, subject)
end
+ def confirm_deletion_with_password?
+ !password_automatically_set? && allow_password_authentication?
+ end
+
def first_name
name.split.first unless name.blank?
end
@@ -651,20 +678,13 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count
end
- def recent_push(project_ids = nil)
- # Get push events not earlier than 2 hours ago
- events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
- events = events.where(project_id: project_ids) if project_ids
-
- # Use the latest event that has not been pushed or merged recently
- events.includes(:project).recent.find do |event|
- next unless event.project.repository.branch_exists?(event.branch_name)
-
- merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
- .where(source_project_id: event.project.id,
- source_branch: event.branch_name)
+ def recent_push(project = nil)
+ service = Users::LastPushEventService.new(self)
- merge_requests.empty?
+ if project
+ service.last_event_for_project(project)
+ else
+ service.last_event_for_user
end
end
@@ -685,19 +705,15 @@ class User < ActiveRecord::Base
end
def fork_of(project)
- links = ForkedProjectLink.where(
- forked_from_project_id: project,
- forked_to_project_id: personal_projects.unscope(:order)
- )
- if links.any?
- links.first.forked_to_project
- else
- nil
- end
+ namespace.find_fork_of(project)
end
def ldap_user?
- identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
+ if identities.loaded?
+ identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
+ else
+ identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
+ end
end
def ldap_identity
@@ -817,6 +833,10 @@ class User < ActiveRecord::Base
avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end
+ def primary_email_verified?
+ confirmed? && !temp_oauth_email?
+ end
+
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
@@ -824,6 +844,18 @@ class User < ActiveRecord::Base
all_emails
end
+ def verified_emails
+ verified_emails = []
+ verified_emails << email if primary_email_verified?
+ verified_emails.concat(emails.confirmed.pluck(:email))
+ verified_emails
+ end
+
+ def verified_email?(check_email)
+ downcased = check_email.downcase
+ email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
+ end
+
def hook_attrs
{
name: name,
@@ -1007,7 +1039,7 @@ class User < ActiveRecord::Base
if attempts_exceeded?
lock_access! unless access_locked?
else
- Users::UpdateService.new(self).execute(validate: false)
+ Users::UpdateService.new(self, user: self).execute(validate: false)
end
end
@@ -1048,10 +1080,6 @@ class User < ActiveRecord::Base
ensure_rss_token!
end
- def verified_email?(email)
- self.email == email
- end
-
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email
@@ -1068,6 +1096,12 @@ class User < ActiveRecord::Base
user_synced_attributes_metadata&.read_only?(attribute)
end
+ # override, from Devise
+ def lock_access!
+ Gitlab::AppLogger.info("Account Locked: username=#{username}")
+ super
+ end
+
protected
# override, from Devise::Validatable
@@ -1193,7 +1227,7 @@ class User < ActiveRecord::Base
&creation_block
)
- Users::UpdateService.new(user).execute(validate: false)
+ Users::UpdateService.new(user, user: user).execute(validate: false)
user
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
new file mode 100644
index 00000000000..eff25b31f9b
--- /dev/null
+++ b/app/models/user_custom_attribute.rb
@@ -0,0 +1,6 @@
+class UserCustomAttribute < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user_id, :key, :value, presence: true
+ validates :key, uniqueness: { scope: [:user_id] }
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index f2315bb3dbb..5f710961f95 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -50,7 +50,7 @@ class WikiPage
# The Gitlab ProjectWiki instance.
attr_reader :wiki
- # The raw Gollum::Page instance.
+ # The raw Gitlab::Git::WikiPage instance.
attr_reader :page
# The attributes Hash used for storing and validating
@@ -75,7 +75,7 @@ class WikiPage
if @attributes[:slug].present?
@attributes[:slug]
else
- wiki.wiki.preview_page(title, '', format).url_path
+ wiki.wiki.preview_slug(title, format)
end
end
@@ -131,7 +131,7 @@ class WikiPage
def versions
return [] unless persisted?
- @page.versions
+ wiki.wiki.page_versions(@page.path)
end
def commit
@@ -264,8 +264,8 @@ class WikiPage
end
page_title, page_dir = wiki.page_title_and_dir(page_details)
- gollum_wiki = wiki.wiki
- @page = gollum_wiki.paged(page_title, page_dir)
+ gitlab_git_wiki = wiki.wiki
+ @page = gitlab_git_wiki.page(title: page_title, dir: page_dir)
set_attributes
@persisted = errors.blank?
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb
new file mode 100644
index 00000000000..e77173ea6e1
--- /dev/null
+++ b/app/policies/gcp/cluster_policy.rb
@@ -0,0 +1,12 @@
+module Gcp
+ class ClusterPolicy < BasePolicy
+ alias_method :cluster, :subject
+
+ delegate { @subject.project }
+
+ rule { can?(:master_access) }.policy do
+ enable :update_cluster
+ enable :admin_cluster
+ end
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 1be7bbe9953..64e550d19d0 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy
with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? }
+ condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+
rule { anonymous }.policy do
prevent :log_in
prevent :access_api
@@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy
enable :create_group
end
+ rule { can_create_fork }.policy do
+ enable :create_fork
+ end
+
rule { access_locked }.policy do
prevent :log_in
end
@@ -47,4 +53,9 @@ class GlobalPolicy < BasePolicy
rule { ~(anonymous & restricted_public_level) }.policy do
enable :read_users_list
end
+
+ rule { admin }.policy do
+ enable :read_custom_attribute
+ enable :update_custom_attribute
+ end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 420991ff6d6..8af9738d75c 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -9,6 +9,7 @@ class GroupPolicy < BasePolicy
condition(:has_access) { access_level != GroupMember::NO_ACCESS }
condition(:guest) { access_level >= GroupMember::GUEST }
+ condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER }
condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
@@ -33,11 +34,11 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
+ rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
rule { master }.policy do
enable :create_projects
- enable :admin_milestones
enable :admin_pipeline
enable :admin_build
end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index daf6fa9e18a..f0aa16d2ecf 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -1,6 +1,10 @@
class IssuablePolicy < BasePolicy
delegate { @subject.project }
+ condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
+
+ condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
+
desc "User is the assignee or author"
condition(:assignee_or_author) do
@user && @subject.assignee_or_author?(@user)
@@ -12,4 +16,12 @@ class IssuablePolicy < BasePolicy
enable :read_merge_request
enable :update_merge_request
end
+
+ rule { locked & ~is_project_member }.policy do
+ prevent :create_note
+ prevent :update_note
+ prevent :admin_note
+ prevent :resolve_note
+ prevent :edit_note
+ end
end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 85b67f0a237..92213f0155e 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -1,10 +1,14 @@
class NamespacePolicy < BasePolicy
rule { anonymous }.prevent_all
+ condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
+ condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
condition(:owner) { @subject.owner == @user }
rule { owner | admin }.policy do
enable :create_projects
enable :admin_namespace
end
+
+ rule { personal_project & ~can_create_personal_project }.prevent :create_projects
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 20cd51cfb99..d4cb5a77e63 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -1,5 +1,6 @@
class NotePolicy < BasePolicy
delegate { @subject.project }
+ delegate { @subject.noteable if @subject.noteable.lockable? }
condition(:is_author) { @user && @subject.author == @user }
condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
@@ -8,6 +9,7 @@ class NotePolicy < BasePolicy
condition(:editable, scope: :subject) { @subject.editable? }
rule { ~editable | anonymous }.prevent :edit_note
+
rule { is_author | admin }.enable :edit_note
rule { can?(:master_access) }.enable :edit_note
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a925fac7d3e..f599eab42f2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -155,6 +155,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) }.policy do
enable :admin_merge_request
+ enable :admin_milestone
enable :update_merge_request
enable :create_commit_status
enable :update_commit_status
@@ -178,7 +179,6 @@ class ProjectPolicy < BasePolicy
enable :update_project_snippet
enable :update_environment
enable :update_deployment
- enable :admin_milestone
enable :admin_project_snippet
enable :admin_project_member
enable :admin_note
@@ -193,6 +193,8 @@ class ProjectPolicy < BasePolicy
enable :admin_pages
enable :read_pages
enable :update_pages
+ enable :read_cluster
+ enable :create_cluster
end
rule { can?(:public_user_access) }.policy do
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index a542bdd8295..099b4720fb6 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -1,7 +1,18 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ FAILURE_REASONS = {
+ config_error: 'CI/CD YAML configuration error!'
+ }.freeze
+
presents :pipeline
+ def failure_reason
+ return unless pipeline.failure_reason?
+
+ FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
+ pipeline.failure_reason
+ end
+
def status_title
if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb
new file mode 100644
index 00000000000..f7908f92a37
--- /dev/null
+++ b/app/presenters/gcp/cluster_presenter.rb
@@ -0,0 +1,9 @@
+module Gcp
+ class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ presents :cluster
+
+ def gke_cluster_url
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
+ end
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 2df84e58575..a25882cbb62 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def remove_wip_path
- if can?(current_user, :update_merge_request, merge_request.project)
+ if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project)
remove_wip_project_merge_request_path(project, merge_request)
end
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 743a08acefe..8c89eea607f 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private
def build_failed_issue_options
- { title: "Build Failed ##{build.id}",
- description: project_job_path(project, build) }
+ { title: "Job Failed ##{build.id}",
+ description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end
def current_user
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
new file mode 100644
index 00000000000..08a113c4d8a
--- /dev/null
+++ b/app/serializers/cluster_entity.rb
@@ -0,0 +1,6 @@
+class ClusterEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :status_name, as: :status
+ expose :status_reason
+end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
new file mode 100644
index 00000000000..2c87202a105
--- /dev/null
+++ b/app/serializers/cluster_serializer.rb
@@ -0,0 +1,7 @@
+class ClusterSerializer < BaseSerializer
+ entity ClusterEntity
+
+ def represent_status(resource)
+ represent(resource, { only: [:status, :status_reason] })
+ end
+end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index e4e9d8ef90a..c8dd98cc04d 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -1,4 +1,4 @@
-class CommitEntity < API::Entities::RepoCommit
+class CommitEntity < API::Entities::Commit
include RequestAwareEntity
expose :author, using: UserEntity
diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb
new file mode 100644
index 00000000000..56dc70b5687
--- /dev/null
+++ b/app/serializers/container_repositories_serializer.rb
@@ -0,0 +1,3 @@
+class ContainerRepositoriesSerializer < BaseSerializer
+ entity ContainerRepositoryEntity
+end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
new file mode 100644
index 00000000000..1103cf30a07
--- /dev/null
+++ b/app/serializers/container_repository_entity.rb
@@ -0,0 +1,25 @@
+class ContainerRepositoryEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :location
+
+ expose :tags_path do |repository|
+ project_registry_repository_tags_path(project, repository, format: :json)
+ end
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |repository|
+ project_container_registry_path(project, repository, format: :json)
+ end
+
+ private
+
+ alias_method :repository, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
new file mode 100644
index 00000000000..ec1fc349586
--- /dev/null
+++ b/app/serializers/container_tag_entity.rb
@@ -0,0 +1,23 @@
+class ContainerTagEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name, :location, :revision, :total_size, :created_at
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
+ project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json)
+ end
+
+ private
+
+ alias_method :tag, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ # TODO: We check permission against @project, not tag,
+ # as tag is no AR object that is attached to project
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tags_serializer.rb b/app/serializers/container_tags_serializer.rb
new file mode 100644
index 00000000000..6ff3adff135
--- /dev/null
+++ b/app/serializers/container_tags_serializer.rb
@@ -0,0 +1,17 @@
+class ContainerTagsSerializer < BaseSerializer
+ entity ContainerTagEntity
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index dcaccc3007d..ba0ae6ba8a0 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -26,5 +26,9 @@ class EnvironmentEntity < Grape::Entity
terminal_project_environment_path(environment.project, environment)
end
+ expose :folder_path do |environment|
+ folder_project_environments_path(environment.project, environment.folder_name)
+ end
+
expose :created_at, :updated_at
end
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index d0a60f134da..88842a9aa75 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -36,9 +36,9 @@ class EnvironmentSerializer < BaseSerializer
private
def itemize(resource)
- items = resource.order('folder_name ASC')
+ items = resource.order('folder ASC')
.group('COALESCE(environment_type, name)')
- .select('COALESCE(environment_type, name) AS folder_name',
+ .select('COALESCE(environment_type, name) AS folder',
'COUNT(*) AS size', 'MAX(id) AS last_id')
# It makes a difference when you call `paginate` method, because
@@ -49,7 +49,7 @@ class EnvironmentSerializer < BaseSerializer
environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
items.map do |item|
- Item.new(item.folder_name, item.size, environments[item.last_id])
+ Item.new(item.folder, item.size, environments[item.last_id])
end
end
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 0d6feb78173..10d3ad0214b 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -3,6 +3,7 @@ class IssueEntity < IssuableEntity
expose :branch_name
expose :confidential
+ expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
expose :due_date
expose :moved_to_id
@@ -14,7 +15,7 @@ class IssueEntity < IssuableEntity
expose :current_user do
expose :can_create_note do |issue|
- can?(request.current_user, :create_note, issue.project)
+ can?(request.current_user, :create_note, issue)
end
expose :can_update do |issue|
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 07650ce6f20..297a459e394 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -13,12 +13,16 @@ class MergeRequestEntity < IssuableEntity
expose :target_branch
expose :target_project_id
+ expose :should_be_rebased?, as: :should_be_rebased
+ expose :ff_only_enabled do |merge_request|
+ merge_request.project.merge_requests_ff_only_enabled
+ end
+
# Events
expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity
# User entities
- expose :author, using: UserEntity
expose :merge_user, using: UserEntity
# Diff sha's
@@ -26,7 +30,6 @@ class MergeRequestEntity < IssuableEntity
merge_request.diff_head_sha if merge_request.diff_head_commit
end
- expose :merge_commit_sha
expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
@@ -39,6 +42,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
+ expose :mergeable?, as: :mergeable
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 357fc71f877..6457294b285 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
+ expose :failure_reason?, as: :failure_reason
end
expose :details do
@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity
end
expose :commit, using: CommitEntity
+ expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
+
+ expose :failure_reason, if: -> (pipeline, _) { pipeline.failure_reason? } do |pipeline|
+ pipeline.present.failure_reason
+ end
expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline)
@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
- expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
-
private
alias_method :pipeline, :object
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
new file mode 100644
index 00000000000..f7ee0e468e2
--- /dev/null
+++ b/app/services/ci/create_cluster_service.rb
@@ -0,0 +1,15 @@
+module Ci
+ class CreateClusterService < BaseService
+ def execute(access_token)
+ params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
+
+ cluster_params =
+ params.merge(user: current_user,
+ gcp_token: access_token)
+
+ project.create_cluster(cluster_params).tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index d20de9b16a4..31a712ccc1b 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -2,110 +2,55 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil)
+ SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
+ Gitlab::Ci::Pipeline::Chain::Validate::Repository,
+ Gitlab::Ci::Pipeline::Chain::Validate::Config,
+ Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::Create].freeze
+
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new(
source: source,
project: project,
ref: ref,
sha: sha,
before_sha: before_sha,
- tag: tag?,
+ tag: tag_exists?,
trigger_requests: Array(trigger_request),
user: current_user,
pipeline_schedule: schedule,
protected: project.protected_for?(ref)
)
- result = validate_project_and_git_items ||
- validate_pipeline(ignore_skip_ci: ignore_skip_ci,
- save_on_errors: save_on_errors)
+ command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
+ save_incompleted: save_on_errors,
+ seeds_block: block,
+ project: project,
+ current_user: current_user)
- return result if result
+ sequence = Gitlab::Ci::Pipeline::Chain::Sequence
+ .new(pipeline, command, SEQUENCE)
- begin
- Ci::Pipeline.transaction do
- pipeline.save!
+ sequence.build! do |pipeline, sequence|
+ update_merge_requests_head_pipeline if pipeline.persisted?
- yield(pipeline) if block_given?
+ if sequence.complete?
+ cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
+ pipeline_created_counter.increment(source: source)
- Ci::CreatePipelineStagesService
- .new(project, current_user)
- .execute(pipeline)
+ pipeline.process!
end
- rescue ActiveRecord::RecordInvalid => e
- return error("Failed to persist the pipeline: #{e}")
end
-
- update_merge_requests_head_pipeline
-
- cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
-
- pipeline_created_counter.increment(source: source)
-
- pipeline.tap(&:process!)
end
private
- def validate_project_and_git_items
- unless project.builds_enabled?
- return error('Pipeline is disabled')
- end
-
- unless allowed_to_trigger_pipeline?
- if can?(current_user, :create_pipeline, project)
- return error("Insufficient permissions for protected ref '#{ref}'")
- else
- return error('Insufficient permissions to create a new pipeline')
- end
- end
-
- unless branch? || tag?
- return error('Reference not found')
- end
-
- unless commit
- return error('Commit not found')
- end
- end
-
- def validate_pipeline(ignore_skip_ci:, save_on_errors:)
- unless pipeline.config_processor
- unless pipeline.ci_yaml_file
- return error("Missing #{pipeline.ci_yaml_file_path} file")
- end
- return error(pipeline.yaml_errors, save: save_on_errors)
- end
-
- if !ignore_skip_ci && skip_ci?
- pipeline.skip if save_on_errors
- return pipeline
- end
-
- unless pipeline.has_stage_seeds?
- return error('No stages / jobs for this pipeline.')
- end
- end
-
- def allowed_to_trigger_pipeline?
- if current_user
- allowed_to_create?
- else # legacy triggers don't have a corresponding user
- !project.protected_for?(ref)
- end
+ def commit
+ @commit ||= project.commit(origin_sha || origin_ref)
end
- def allowed_to_create?
- return unless can?(current_user, :create_pipeline, project)
-
- access = Gitlab::UserAccess.new(current_user, project: project)
- if branch?
- access.can_update_branch?(ref)
- elsif tag?
- access.can_create_tag?(ref)
- else
- true # Allow it for now and we'll reject when we check ref existence
- end
+ def sha
+ commit.try(:id)
end
def update_merge_requests_head_pipeline
@@ -115,11 +60,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id)
end
- def skip_ci?
- return false unless pipeline.git_commit_message
- pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
- end
-
def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable|
@@ -136,14 +76,6 @@ module Ci
.created_or_pending
end
- def commit
- @commit ||= project.commit(origin_sha || origin_ref)
- end
-
- def sha
- commit.try(:id)
- end
-
def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end
@@ -156,41 +88,17 @@ module Ci
params[:ref]
end
- def branch?
- return @is_branch if defined?(@is_branch)
-
- @is_branch =
- project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
- end
-
- def tag?
- return @is_tag if defined?(@is_tag)
-
- @is_tag =
- project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+ def tag_exists?
+ project.repository.tag_exists?(ref)
end
def ref
@ref ||= Gitlab::Git.ref_name(origin_ref)
end
- def valid_sha?
- origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
- end
-
- def error(message, save: false)
- pipeline.tap do
- pipeline.errors.add(:base, message)
-
- if save
- pipeline.drop
- update_merge_requests_head_pipeline
- end
- end
- end
-
def pipeline_created_counter
- @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created")
+ @pipeline_created_counter ||= Gitlab::Metrics
+ .counter(:pipelines_created_total, "Counter of pipelines created")
end
end
end
diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb
new file mode 100644
index 00000000000..75f9e0f897d
--- /dev/null
+++ b/app/services/ci/extract_sections_from_build_trace_service.rb
@@ -0,0 +1,30 @@
+module Ci
+ class ExtractSectionsFromBuildTraceService < BaseService
+ def execute(build)
+ return false unless build.trace_sections.empty?
+
+ Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build))
+ true
+ end
+
+ private
+
+ def find_or_create_name(name)
+ project.build_trace_section_names.find_or_create_by!(name: name)
+ rescue ActiveRecord::RecordInvalid
+ project.build_trace_section_names.find_by!(name: name)
+ end
+
+ def extract_sections(build)
+ build.trace.extract_sections.map do |attr|
+ name = attr.delete(:name)
+ name_record = find_or_create_name(name)
+
+ attr.merge(
+ build_id: build.id,
+ project_id: project.id,
+ section_name_id: name_record.id)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
new file mode 100644
index 00000000000..0b68e4d6ea9
--- /dev/null
+++ b/app/services/ci/fetch_gcp_operation_service.rb
@@ -0,0 +1,17 @@
+module Ci
+ class FetchGcpOperationService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ operation = api_client.projects_zones_operations(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+end
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
new file mode 100644
index 00000000000..44da87cb00c
--- /dev/null
+++ b/app/services/ci/fetch_kubernetes_token_service.rb
@@ -0,0 +1,72 @@
+##
+# TODO:
+# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb
+# We should dry up those classes not to repeat the same code.
+# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller.
+module Ci
+ class FetchKubernetesTokenService
+ attr_reader :api_url, :ca_pem, :username, :password
+
+ def initialize(api_url, ca_pem, username, password)
+ @api_url = api_url
+ @ca_pem = ca_pem
+ @username = username
+ @password = password
+ end
+
+ def execute
+ read_secrets.each do |secret|
+ name = secret.dig('metadata', 'name')
+ if /default-token/ =~ name
+ token_base64 = secret.dig('data', 'token')
+ return Base64.decode64(token_base64) if token_base64
+ end
+ end
+
+ nil
+ end
+
+ private
+
+ def read_secrets
+ kubeclient = build_kubeclient!
+
+ kubeclient.get_secrets.as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+ []
+ end
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && username && password
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: { username: username, password: password },
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def join_api_url(api_path)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [prefix, api_path].join("/")
+
+ url.to_s
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+ end
+end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
new file mode 100644
index 00000000000..347875c5697
--- /dev/null
+++ b/app/services/ci/finalize_cluster_creation_service.rb
@@ -0,0 +1,33 @@
+module Ci
+ class FinalizeClusterCreationService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ begin
+ gke_cluster = api_client.projects_zones_clusters_get(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_cluster_name)
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+
+ endpoint = gke_cluster.endpoint
+ api_url = 'https://' + endpoint
+ ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
+ username = gke_cluster.master_auth.username
+ password = gke_cluster.master_auth.password
+
+ kubernetes_token = Ci::FetchKubernetesTokenService.new(
+ api_url, ca_cert, username, password).execute
+
+ unless kubernetes_token
+ return cluster.make_errored!('Failed to get a default token of kubernetes')
+ end
+
+ Ci::IntegrateClusterService.new.execute(
+ cluster, endpoint, ca_cert, kubernetes_token, username, password)
+ end
+ end
+end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
new file mode 100644
index 00000000000..d123ce8d26b
--- /dev/null
+++ b/app/services/ci/integrate_cluster_service.rb
@@ -0,0 +1,26 @@
+module Ci
+ class IntegrateClusterService
+ def execute(cluster, endpoint, ca_cert, token, username, password)
+ Gcp::Cluster.transaction do
+ cluster.update!(
+ enabled: true,
+ endpoint: endpoint,
+ ca_cert: ca_cert,
+ kubernetes_token: token,
+ username: username,
+ password: password,
+ service: cluster.project.find_or_initialize_service('kubernetes'),
+ status_event: :make_created)
+
+ cluster.service.update!(
+ active: true,
+ api_url: cluster.api_url,
+ ca_pem: ca_cert,
+ namespace: cluster.project_namespace,
+ token: token)
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
+ end
+ end
+end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 1e5ad28ba57..120af8c1e61 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -14,7 +14,7 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
- trigger.trigger_requests.create!(pipeline: pipeline)
+ pipeline.trigger_requests.create!(trigger: trigger)
create_pipeline_variables!(pipeline)
end
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
new file mode 100644
index 00000000000..52d80b01813
--- /dev/null
+++ b/app/services/ci/provision_cluster_service.rb
@@ -0,0 +1,36 @@
+module Ci
+ class ProvisionClusterService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ begin
+ operation = api_client.projects_zones_clusters_create(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_cluster_name,
+ cluster.gcp_cluster_size,
+ machine_type: cluster.gcp_machine_type)
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+
+ unless operation.status == 'RUNNING' || operation.status == 'PENDING'
+ return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
+
+ unless cluster.gcp_operation_id
+ return cluster.make_errored!('Can not find operation_id from self_link')
+ end
+
+ if cluster.make_creating
+ WaitForClusterCreationWorker.perform_in(
+ WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
+ else
+ return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
+ end
+ end
+ end
+end
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
new file mode 100644
index 00000000000..70d88fca660
--- /dev/null
+++ b/app/services/ci/update_cluster_service.rb
@@ -0,0 +1,22 @@
+module Ci
+ class UpdateClusterService < BaseService
+ def execute(cluster)
+ Gcp::Cluster.transaction do
+ cluster.update!(params)
+
+ if params['enabled'] == 'true'
+ cluster.service.update!(
+ active: true,
+ api_url: cluster.api_url,
+ ca_pem: cluster.ca_cert,
+ namespace: cluster.project_namespace,
+ token: cluster.kubernetes_token)
+ else
+ cluster.service.update!(active: false)
+ end
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ cluster.errors.add(:base, e.message)
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 85c2fcf9ea6..b9d0173a2d0 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -12,14 +12,18 @@ module Commits
raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
+ message = @commit.public_send(:"#{action}_message", current_user)
+
+ # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
@commit,
@branch_name,
+ message,
start_project: @start_project,
start_branch_name: @start_branch)
- rescue Repository::CreateTreeError
+ rescue Gitlab::Git::Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
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
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index ff11bd59d29..077268b2388 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -6,15 +6,18 @@ class DeleteMergedBranchesService < BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
- 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
- # Prevent deletion of protected branches
- branches = branches.reject { |branch| project.protected_for?(branch) }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ 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
+ # Prevent deletion of protected branches
+ branches = branches.reject { |branch| project.protected_for?(branch) }
- branches.each do |branch|
- DeleteBranchService.new(project, current_user).execute(branch)
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
end
end
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
new file mode 100644
index 00000000000..16de3d08df2
--- /dev/null
+++ b/app/services/deploy_keys/create_service.rb
@@ -0,0 +1,7 @@
+module DeployKeys
+ class CreateService < Keys::BaseService
+ def execute
+ DeployKey.create(params.merge(user: user))
+ end
+ end
+end
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index ace49889097..5bbceeb3b3f 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -1,8 +1,8 @@
module Emails
class BaseService
- def initialize(user, opts)
- @user = user
- @email = opts[:email]
+ def initialize(current_user, params = {})
+ @current_user, @params = current_user, params.dup
+ @user = params.delete(:user)
end
end
end
diff --git a/app/services/emails/confirm_service.rb b/app/services/emails/confirm_service.rb
new file mode 100644
index 00000000000..b5301bf2b82
--- /dev/null
+++ b/app/services/emails/confirm_service.rb
@@ -0,0 +1,7 @@
+module Emails
+ class ConfirmService < ::Emails::BaseService
+ def execute(email)
+ email.resend_confirmation_instructions
+ end
+ end
+end
diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb
index b6491ee9804..94a841af7c3 100644
--- a/app/services/emails/create_service.rb
+++ b/app/services/emails/create_service.rb
@@ -1,7 +1,7 @@
module Emails
class CreateService < ::Emails::BaseService
- def execute
- @user.emails.create(email: @email)
+ def execute(extra_params = {})
+ @user.emails.create(@params.merge(extra_params))
end
end
end
diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb
index d586b9dfe0c..1ed131fe326 100644
--- a/app/services/emails/destroy_service.rb
+++ b/app/services/emails/destroy_service.rb
@@ -1,13 +1,13 @@
module Emails
class DestroyService < ::Emails::BaseService
- def execute
- Email.find_by_email!(@email).destroy && update_secondary_emails!
+ def execute(email)
+ email.destroy && update_secondary_emails!
end
private
def update_secondary_emails!
- result = ::Users::UpdateService.new(@user).execute do |user|
+ result = ::Users::UpdateService.new(@current_user, user: @user).execute do |user|
user.update_secondary_emails!
end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 0b7e4f187f7..6328d567a07 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -74,12 +74,19 @@ class EventCreateService
# We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being
# rolled back as well.
- Event.transaction do
- event = create_event(project, current_user, Event::PUSHED)
+ event = Event.transaction do
+ new_event = create_event(project, current_user, Event::PUSHED)
- PushEventPayloadService.new(event, push_data).execute
+ PushEventPayloadService
+ .new(new_event, push_data)
+ .execute
+
+ new_event
end
+ Users::LastPushEventService.new(current_user)
+ .cache_last_push_event(event)
+
Users::ActivityService.new(current_user, 'push').execute
end
diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb
new file mode 100644
index 00000000000..e822a89c4d3
--- /dev/null
+++ b/app/services/gpg_keys/create_service.rb
@@ -0,0 +1,9 @@
+module GpgKeys
+ class CreateService < Keys::BaseService
+ def execute
+ key = user.gpg_keys.create(params)
+ notification_service.new_gpg_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 8b967b78052..f83ece7098f 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -43,6 +43,10 @@ class IssuableBaseService < BaseService
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
end
+ def create_discussion_lock_note(issuable)
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
@@ -57,6 +61,7 @@ class IssuableBaseService < BaseService
params.delete(:due_date)
params.delete(:canonical_issue_id)
params.delete(:project)
+ params.delete(:discussion_locked)
end
filter_assignee(issuable)
@@ -182,6 +187,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
+ issuable.update_project_counter_caches
end
issuable
@@ -193,8 +199,6 @@ class IssuableBaseService < BaseService
def after_create(issuable)
# To be overridden by subclasses
-
- issuable.update_project_counter_caches
end
def before_update(issuable)
@@ -203,8 +207,6 @@ class IssuableBaseService < BaseService
def after_update(issuable)
# To be overridden by subclasses
-
- issuable.update_project_counter_caches
end
def update(issuable)
@@ -229,12 +231,17 @@ class IssuableBaseService < BaseService
before_update(issuable)
+ # We have to perform this check before saving the issuable as Rails resets
+ # the changed fields upon calling #save.
+ update_project_counters = issuable.update_project_counter_caches?
+
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels)
end
+ change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
@@ -249,6 +256,8 @@ class IssuableBaseService < BaseService
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
+
+ issuable.update_project_counter_caches if update_project_counters
end
end
@@ -291,6 +300,12 @@ class IssuableBaseService < BaseService
end
end
+ def change_discussion_lock(issuable)
+ if issuable.previous_changes.include?('discussion_locked')
+ create_discussion_lock_note(issuable)
+ end
+ end
+
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 74459c3342c..0c5cf2c62ad 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -29,6 +29,7 @@ module Issues
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
+ issue.update_project_counter_caches
end
issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b4ca3966505..e0339ddf9bb 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -34,7 +34,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
new file mode 100644
index 00000000000..545832d0bd4
--- /dev/null
+++ b/app/services/keys/base_service.rb
@@ -0,0 +1,13 @@
+module Keys
+ class BaseService
+ attr_accessor :user, :params
+
+ def initialize(user, params)
+ @user, @params = user, params
+ end
+
+ def notification_service
+ NotificationService.new
+ end
+ end
+end
diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb
new file mode 100644
index 00000000000..e2e5a6c46c5
--- /dev/null
+++ b/app/services/keys/create_service.rb
@@ -0,0 +1,9 @@
+module Keys
+ class CreateService < ::Keys::BaseService
+ def execute
+ key = user.keys.create(params)
+ notification_service.new_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/keys/last_used_service.rb b/app/services/keys/last_used_service.rb
new file mode 100644
index 00000000000..dbd79f7da55
--- /dev/null
+++ b/app/services/keys/last_used_service.rb
@@ -0,0 +1,35 @@
+module Keys
+ class LastUsedService
+ TIMEOUT = 1.day.to_i
+
+ attr_reader :key
+
+ # key - The Key for which to update the last used timestamp.
+ def initialize(key)
+ @key = key
+ end
+
+ def execute
+ # We _only_ want to update last_used_at and not also updated_at (which
+ # would be updated when using #touch).
+ key.update_column(:last_used_at, Time.zone.now) if update?
+ end
+
+ def update?
+ return false if ::Gitlab::Database.read_only?
+
+ last_used = key.last_used_at
+
+ return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
+
+ !!redis_lease.try_obtain
+ end
+
+ private
+
+ def redis_lease
+ Gitlab::ExclusiveLease
+ .new("key_update_last_used_at:#{key.id}", timeout: TIMEOUT)
+ end
+ end
+end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index c0ce01f7523..40213c99014 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -14,6 +14,7 @@ module MergeRequests
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
+ merge_request.update_project_counter_caches
end
merge_request
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 3d53fe0646b..820709583fa 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -13,7 +13,10 @@ module MergeRequests
merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
- create(merge_request)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37439
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ create(merge_request)
+ end
end
def before_create(merge_request)
diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
new file mode 100644
index 00000000000..ba6853b835a
--- /dev/null
+++ b/app/services/merge_requests/ff_merge_service.rb
@@ -0,0 +1,24 @@
+module MergeRequests
+ # MergeService class
+ #
+ # Do git fast-forward merge and in case of success
+ # mark merge request as merged and execute all hooks and notifications
+ # Executed when you do fast-forward merge via GitLab UI
+ #
+ class FfMergeService < MergeRequests::MergeService
+ private
+
+ def commit
+ repository.ff_merge(current_user,
+ source,
+ merge_request.target_branch,
+ merge_request: merge_request)
+ rescue Gitlab::Git::HooksService::PreReceiveError => e
+ raise MergeError, e.message
+ rescue StandardError => e
+ raise MergeError, "Something went wrong during merge: #{e.message}"
+ ensure
+ merge_request.update(in_progress_merge_commit_sha: nil)
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b2b6c5627fb..a110abf8256 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -11,16 +11,21 @@ module MergeRequests
attr_reader :merge_request, :source
def execute(merge_request)
+ if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
+ FfMergeService.new(project, current_user, params).execute(merge_request)
+ return
+ end
+
@merge_request = merge_request
unless @merge_request.mergeable?
- return log_merge_error('Merge request is not mergeable', save_message_on_model: true)
+ return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
end
@source = find_merge_source
unless @source
- return log_merge_error('No source for merge', save_message_on_model: true)
+ return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
end
merge_request.in_locked_state do
@@ -31,22 +36,15 @@ module MergeRequests
end
end
rescue MergeError => e
- clean_merge_jid
- log_merge_error(e.message, save_message_on_model: true)
+ handle_merge_error(log_message: e.message, save_message_on_model: true)
end
private
def commit
- committer = repository.user_to_committer(current_user)
+ message = params[:commit_message] || merge_request.merge_commit_message
- options = {
- message: params[:commit_message] || merge_request.merge_commit_message,
- author: committer,
- committer: committer
- }
-
- commit_id = repository.merge(current_user, source, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, message)
raise MergeError, 'Conflicts detected during merge' unless commit_id
@@ -80,10 +78,16 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
- def log_merge_error(message, save_message_on_model: false)
- Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
+ # Logs merge error message and cleans `MergeRequest#merge_jid`.
+ #
+ def handle_merge_error(log_message:, save_message_on_model: false)
+ Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
- @merge_request.update(merge_error: message) if save_message_on_model
+ if save_message_on_model
+ @merge_request.update(merge_error: log_message, merge_jid: nil)
+ else
+ clean_merge_jid
+ end
end
def merge_request_info
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 261a8bfa200..b1d6bac4d4a 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -14,6 +14,7 @@ module MergeRequests
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
+ merge_request.update_project_counter_caches
end
private
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 06971483992..9ea28733f5f 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -4,7 +4,13 @@ module Notes
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = Notes::BuildService.new(project, current_user, params).execute
- return note unless note.valid?
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37440
+ note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ note.valid?
+ end
+
+ return note unless note_valid
# We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e2a80db06a6..8d5da459882 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -31,13 +31,6 @@ class NotificationService
end
end
- # Always notify user about email added to profile
- def new_email(email)
- if email.user&.can?(:receive_notifications)
- mailer.new_email_email(email.id).deliver_later
- end
- end
-
# When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index 5e633c37bf8..aa034315280 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -2,6 +2,11 @@ module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
class CountService
+ # The version of the cache format. This should be bumped whenever the
+ # underlying logic changes. This removes the need for explicitly flushing
+ # all caches.
+ VERSION = 1
+
def initialize(project)
@project = project
end
@@ -37,7 +42,7 @@ module Projects
end
def cache_key
- ['projects', @project.id, cache_key_name]
+ ['projects', 'count_service', VERSION, @project.id, cache_key_name]
end
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 54eb75ab9bf..19d75ff2efa 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -22,6 +22,13 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
+ # The project is not necessarily a fork, so update the fork network originating
+ # from this project
+ if fork_network = project.root_of_fork_network
+ fork_network.update(root_project: nil,
+ deleted_root_project_name: project.full_name)
+ end
+
attempt_destroy_transaction(project)
system_hook_service.execute_hooks_for(project, :destroy)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index ad67e68a86a..eb5cce5ab98 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -23,11 +23,31 @@ module Projects
refresh_forks_count
+ link_fork_network(new_project)
+
new_project
end
private
+ def fork_network
+ if @project.fork_network
+ @project.fork_network
+ elsif forked_from_project = @project.forked_from_project
+ # TODO: remove this case when all background migrations have completed
+ # this only happens when a project had a `forked_project_link` that was
+ # not migrated to the `fork_network` relation
+ forked_from_project.fork_network || forked_from_project.create_root_of_fork_network
+ else
+ @project.create_root_of_fork_network
+ end
+ end
+
+ def link_fork_network(new_project)
+ fork_network.fork_network_members.create(project: new_project,
+ forked_from_project: @project)
+ end
+
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
new file mode 100644
index 00000000000..41259de3a16
--- /dev/null
+++ b/app/services/projects/hashed_storage_migration_service.rb
@@ -0,0 +1,68 @@
+module Projects
+ class HashedStorageMigrationService < BaseService
+ include Gitlab::ShellAdapter
+
+ attr_reader :old_disk_path, :new_disk_path
+
+ def initialize(project, logger = nil)
+ @project = project
+ @logger ||= Rails.logger
+ end
+
+ def execute
+ return if project.hashed_storage?
+
+ @old_disk_path = project.disk_path
+ has_wiki = project.wiki.repository_exists?
+
+ project.storage_version = Storage::HashedProject::STORAGE_VERSION
+ project.ensure_storage_path_exists
+
+ @new_disk_path = project.disk_path
+
+ result = move_repository(@old_disk_path, @new_disk_path)
+
+ if has_wiki
+ result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
+ end
+
+ unless result
+ rollback_folder_move
+ return
+ end
+
+ project.repository_read_only = false
+ project.save!
+
+ block_given? ? yield : result
+ end
+
+ private
+
+ def move_repository(from_name, to_name)
+ from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
+
+ # If we don't find the repository on either original or target we should log that as it could be an issue if the
+ # project was not originally empty.
+ if !from_exists && !to_exists
+ logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
+ return false
+ elsif !from_exists
+ # Repository have been moved already.
+ return true
+ end
+
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ def rollback_folder_move
+ move_repository(@new_disk_path, @old_disk_path)
+ move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
+ end
+
+ def logger
+ @logger
+ end
+ end
+end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index f30b40423c8..abe414d0c05 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -16,6 +16,7 @@ module Projects
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy
+ @project.fork_network_member.destroy
end
def refresh_forks_count(project)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index cb4ffcab778..13e292a18bf 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -24,7 +24,10 @@ module Projects
success
else
- error('Project could not be updated!')
+ model_errors = project.errors.full_messages.to_sentence
+ error_message = model_errors.presence || 'Project could not be updated!'
+
+ error(error_message)
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1f66a2668f9..7b32e215c7f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -591,6 +591,13 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
+ def discussion_lock(issuable, author)
+ action = issuable.discussion_locked? ? 'locked' : 'unlocked'
+ body = "#{action} this issue"
+
+ create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index b3f4a72d6fe..cc76d0df3a1 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -11,7 +11,7 @@ module Tags
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
- rescue Rugged::TagError
+ rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists")
rescue Gitlab::Git::HooksService::PreReceiveError => ex
return error(ex.message)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 6ee96d6a0f8..b6125cafa83 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -43,8 +43,8 @@ class TodoService
#
# * create a pending todo for new assignee if issue is assigned
#
- def reassigned_issue(issue, current_user)
- create_assignment_todo(issue, current_user)
+ def reassigned_issue(issue, current_user, old_assignees = [])
+ create_assignment_todo(issue, current_user, old_assignees)
end
# When create a merge request we should:
@@ -254,10 +254,11 @@ class TodoService
create_mention_todos(project, target, author, note, skip_users)
end
- def create_assignment_todo(issuable, author)
+ def create_assignment_todo(issuable, author, old_assignees = [])
if issuable.assignees.any?
+ assignees = issuable.assignees - old_assignees
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
- create_todos(issuable.assignees, attributes)
+ create_todos(assignees, attributes)
end
end
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index ab532a1fdcf..5803404c3c8 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -14,7 +14,7 @@ module Users
private
def record_activity
- Gitlab::UserActivities.record(@author.id)
+ Gitlab::UserActivities.record(@author.id) if Gitlab::Database.read_write?
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb
new file mode 100644
index 00000000000..f2bfb60604f
--- /dev/null
+++ b/app/services/users/last_push_event_service.rb
@@ -0,0 +1,83 @@
+module Users
+ # Service class for caching and retrieving the last push event of a user.
+ class LastPushEventService
+ EXPIRATION = 2.hours
+
+ def initialize(user)
+ @user = user
+ end
+
+ # Caches the given push event for the current user in the Rails cache.
+ #
+ # event - An instance of PushEvent to cache.
+ def cache_last_push_event(event)
+ keys = [
+ project_cache_key(event.project),
+ user_cache_key
+ ]
+
+ if event.project.forked?
+ keys << project_cache_key(event.project.forked_from_project)
+ end
+
+ keys.each { |key| set_key(key, event.id) }
+ end
+
+ # Returns the last PushEvent for the current user.
+ #
+ # This method will return nil if no event was found.
+ def last_event_for_user
+ find_cached_event(user_cache_key)
+ end
+
+ # Returns the last PushEvent for the current user and the given project.
+ #
+ # project - An instance of Project for which to retrieve the PushEvent.
+ #
+ # This method will return nil if no event was found.
+ def last_event_for_project(project)
+ find_cached_event(project_cache_key(project))
+ end
+
+ def find_cached_event(cache_key)
+ event_id = get_key(cache_key)
+
+ return unless event_id
+
+ unless (event = find_event_in_database(event_id))
+ # We don't want to keep querying the same data over and over when a
+ # merge request has been created, thus we remove the key if no event
+ # (meaning an MR was created) is returned.
+ Rails.cache.delete(cache_key)
+ end
+
+ event
+ end
+
+ private
+
+ def find_event_in_database(id)
+ PushEvent
+ .without_existing_merge_requests
+ .find_by(id: id)
+ end
+
+ def user_cache_key
+ "last-push-event/#{@user.id}"
+ end
+
+ def project_cache_key(project)
+ "last-push-event/#{@user.id}/#{project.id}"
+ end
+
+ def get_key(key)
+ Rails.cache.read(key, raw: true)
+ end
+
+ def set_key(key, value)
+ # We're using raw values here since this takes up less space and we don't
+ # store complex objects.
+ Rails.cache.write(key, value, raw: true, expires_in: EXPIRATION)
+ end
+ end
+end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index 6188b8a4349..15ca1a55a5b 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -2,22 +2,21 @@ module Users
class UpdateService < BaseService
include NewUserNotifier
- def initialize(user, params = {})
- @user = user
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @user = params.delete(:user)
@params = params.dup
end
def execute(validate: true, &block)
yield(@user) if block_given?
- assign_attributes(&block)
-
user_exists = @user.persisted?
- if @user.save(validate: validate)
- notify_new_user(@user, nil) unless user_exists
+ assign_attributes(&block)
- success
+ if @user.save(validate: validate)
+ notify_success(user_exists)
else
error(@user.errors.full_messages.uniq.join('. '))
end
@@ -33,6 +32,12 @@ module Users
private
+ def notify_success(user_exists)
+ notify_new_user(@user, nil) unless user_exists
+
+ success
+ end
+
def assign_attributes(&block)
if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 66d3bcb998a..cbb79376d5f 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -9,7 +9,7 @@ class AvatarUploader < GitlabUploader
end
def exists?
- model.avatar.file && model.avatar.file.exists?
+ model.avatar.file && model.avatar.file.present?
end
# We set move_to_store and move_to_cache to 'false' to prevent stealing
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 05a2091633a..7f72b3ce471 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -51,7 +51,7 @@ class GitlabUploader < CarrierWave::Uploader::Base
end
def exists?
- file.try(:exists?)
+ file.present?
end
# Override this if you don't want to save files by default to the Rails.root directory
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index e403a9da616..935787d1a4a 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -21,7 +21,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
@@ -38,7 +38,7 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index e5842bd1ea0..3ef8f2a3acb 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Background Jobs"
-= render 'admin/monitoring/head'
%div{ class: container_class }
%h3.page-title Background Jobs
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
index bff53da1d9a..5e9a8c083af 100644
--- a/app/views/admin/cohorts/index.html.haml
+++ b/app/views/admin/cohorts/index.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Cohorts"
- @no_container = true
-= render "admin/dashboard/head"
%div{ class: container_class }
- if @cohorts
diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml
index 833d4c612f8..30dd87f0463 100644
--- a/app/views/admin/conversational_development_index/show.html.haml
+++ b/app/views/admin/conversational_development_index/show.html.haml
@@ -1,8 +1,6 @@
- @no_container = true
- page_title 'ConvDev Index'
-= render 'admin/monitoring/head'
-
.container
- if show_callout?('convdev_intro_callout_dismissed')
= render 'callout'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
deleted file mode 100644
index c2151710884..00000000000
--- a/app/views/admin/dashboard/_head.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_jobs_path, title: 'Jobs' do
- %span
- Jobs
- = nav_link path: ['runners#index', 'runners#show'] do
- = 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
- = nav_link(controller: :conversational_development_index) do
- = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do
- %span
- ConvDev Index
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 069f8f89e0b..2f0143c7eff 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,13 +1,12 @@
- @no_container = true
- breadcrumb_title "Dashboard"
-= render "admin/dashboard/head"
%div{ class: container_class }
.admin-dashboard.prepend-top-default
.row
.col-md-4
.info-well
- .well-segment.admin-well
+ .well-segment.admin-well.admin-well-statistics
%h4 Statistics
%p
Forks
@@ -43,7 +42,7 @@
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
- .well-segment.admin-well
+ .well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
@@ -112,6 +111,15 @@
%span.pull-right
= API::API::version
%p
+ Gitaly
+ %span.pull-right
+ = Gitlab::GitalyClient.expected_server_version
+ - if Gitlab.config.pages.enabled
+ %p
+ GitLab Pages
+ %span.pull-right
+ = Gitlab::Pages::VERSION
+ %p
Git
%span.pull-right
= Gitlab::Git.version
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index e5f380c78e2..535251fef5e 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Groups"
-= render "admin/dashboard/head"
%div{ class: container_class }
.top-area
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 517db50b97f..10a3bed0a4f 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- page_title _('Health Check')
- no_errors = @errors.blank? && @failing_storage_statuses.blank?
-= render 'admin/monitoring/head'
%div{ class: container_class }
%h3.page-title= page_title
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index fed6002528d..b6e1df5f3ac 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -22,7 +22,7 @@
- @hooks.each do |hook|
%li
.controls
- = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-small'
+ = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: '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
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index aa6e9db3900..7066ed12b95 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -1,10 +1,9 @@
- breadcrumb_title "Jobs"
- @no_container = true
-= render "admin/dashboard/head"
%div{ class: container_class }
- .top-area
+ .top-area.scrolling-tabs-container.inner-page-scroll-tabs
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index ee87f25a225..78757b6384f 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Logs"
-= render 'admin/monitoring/head'
%div{ class: container_class }
%ul.nav-links.log-tabs
diff --git a/app/views/admin/monitoring/_head.html.haml b/app/views/admin/monitoring/_head.html.haml
deleted file mode 100644
index b3530915068..00000000000
--- a/app/views/admin/monitoring/_head.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
- %span
- Requests Profiles
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 3301f55b8a8..4d8754afdd2 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -2,10 +2,9 @@
- page_title "Projects"
- params[:visibility_level] ||= []
-= render "admin/dashboard/head"
%div{ class: container_class }
- .top-area
+ .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.prepend-top-default
.search-holder
= render 'shared/projects/search_form', autofocus: true, icon: true
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index b7db18b2d32..cb02a750490 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title 'Requests Profiles'
-= render 'admin/monitoring/head'
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 6793ce557c4..43cea1358cc 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Runners"
- @no_container = true
-= render "admin/dashboard/head"
%div{ class: container_class }
.bs-callout
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index fd0281e4961..6bf979a937e 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "System Info"
-= render 'admin/monitoring/head'
%div{ class: container_class }
.prepend-top-default
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 5516134d8a0..38ce1564eff 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Users"
-= render "admin/dashboard/head"
%div{ class: container_class }
.prepend-top-default
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
new file mode 100644
index 00000000000..3701e1c0578
--- /dev/null
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -0,0 +1,6 @@
+.top-area
+ %ul.nav-links
+ = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
+ = link_to s_('DashboardProjects|All'), dashboard_projects_path
+ = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
+ = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index c546252455a..57a4da353fe 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -10,11 +10,9 @@
= render "projects/last_push"
%div{ class: container_class }
- - if show_callout?('user_callout_dismissed')
- = render 'shared/user_callout'
-
- - if has_projects_or_name?(@projects, params)
+ - if show_projects?(@projects, params)
= render 'dashboard/projects_head'
+ = render 'nav'
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9b615ec999e..f62a0cd681e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -77,13 +77,14 @@
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.todos-all-done.hidden
- = render "shared/empty_states/icons/todos_all_done.svg"
+ .js-nothing-here-container.todos-all-done.hidden.svg-content
+ = image_tag 'illustrations/todos_all_done.svg'
%h4.text-center
You're all done!
- elsif current_user.todos.any?
.todos-all-done
- = render "shared/empty_states/icons/todos_all_done.svg"
+ .svg-content
+ = image_tag 'illustrations/todos_all_done.svg'
- if todos_filter_empty?
%h4.text-center
= Gitlab.config.gitlab.no_todos_messages.sample
@@ -99,8 +100,8 @@
There are no todos to show.
- else
.todos-empty
- .todos-empty-hero
- = render "shared/empty_states/icons/todos_empty.svg"
+ .todos-empty-hero.svg-content
+ = image_tag 'illustrations/todos_empty.svg'
.todos-empty-content
%h4
Todos let you see what you should do next.
diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
new file mode 100644
index 00000000000..65565b7b8a8
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
@@ -0,0 +1,16 @@
+- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
+- if @resource.unconfirmed_email.present?
+ #content
+ = email_default_heading(@resource.unconfirmed_email)
+ %p Click the link below to confirm your email address.
+ #cta
+ = link_to 'Confirm your email address', confirmation_link
+- else
+ #content
+ - if Gitlab.com?
+ = email_default_heading('Thanks for signing up to GitLab!')
+ - else
+ = email_default_heading("Welcome, #{@resource.name}!")
+ %p To get started, click the link below to confirm your account.
+ #cta
+ = link_to 'Confirm your account', confirmation_link
diff --git a/app/views/devise/mailer/_confirmation_instructions_account.text.erb b/app/views/devise/mailer/_confirmation_instructions_account.text.erb
new file mode 100644
index 00000000000..01f09aa763d
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_account.text.erb
@@ -0,0 +1,14 @@
+<% if @resource.unconfirmed_email.present? %>
+<%= @resource.unconfirmed_email %>,
+
+Use the link below to confirm your email address.
+<% else %>
+ <% if Gitlab.com? %>
+Thanks for signing up to GitLab!
+ <% else %>
+Welcome, <%= @resource.name %>!
+ <% end %>
+To get started, use the link below to confirm your account.
+<% end %>
+
+<%= confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
new file mode 100644
index 00000000000..3d0a1f622a5
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -0,0 +1,8 @@
+#content
+ = email_default_heading("#{@resource.user.name}, you've added an additional email!")
+ %p Click the link below to confirm your email address (#{@resource.email})
+ #cta
+ = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
+ %p
+ If this email was added in error, you can remove it here:
+ = link_to "Emails", profile_emails_url
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
new file mode 100644
index 00000000000..a3b28cb0b84
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
@@ -0,0 +1,7 @@
+<%= @resource.user.name %>, you've added an additional email!
+
+Use the link below to confirm your email address (<%= @resource.email %>)
+
+<%= confirmation_url(@resource, confirmation_token: @token) %>
+
+If this email was added in error, you can remove it here: <%= profile_emails_url %>
diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml
index a508b7537a2..50ee7b53d8f 100644
--- a/app/views/devise/mailer/confirmation_instructions.html.haml
+++ b/app/views/devise/mailer/confirmation_instructions.html.haml
@@ -1,15 +1 @@
-- if @resource.unconfirmed_email.present?
- #content
- = email_default_heading(@resource.unconfirmed_email)
- %p Click the link below to confirm your email address.
- #cta
- = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
-- else
- #content
- - if Gitlab.com?
- = email_default_heading('Thanks for signing up to GitLab!')
- - else
- = email_default_heading("Welcome, #{@resource.name}!")
- %p To get started, click the link below to confirm your account.
- #cta
- = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token)
+= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}"
diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb
index 9f76edb76a4..05fddddf415 100644
--- a/app/views/devise/mailer/confirmation_instructions.text.erb
+++ b/app/views/devise/mailer/confirmation_instructions.text.erb
@@ -1,9 +1 @@
-Welcome, <%= @resource.name %>!
-
-<% if @resource.unconfirmed_email.present? %>
-You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:
-<% else %>
-You can confirm your account through the link below:
-<% end %>
-
-<%= confirmation_url(@resource, confirmation_token: @token) %>
+<%= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" %> \ No newline at end of file
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index bfd7dd25a7d..546cec4d565 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,6 +7,8 @@
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10
- = check_box_tag :remember_me
- = label_tag :remember_me, 'Remember me'
+ %fieldset.prepend-top-10.checkbox.remember-me
+ %label
+ = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ %span
+ Remember me
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index e6d307e5568..52279d0a870 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -1,6 +1,10 @@
-- expanded = local_assigns.fetch(:expanded, true)
-%tr.notes_holder{ class: ('hide' unless expanded) }
- %td.notes_line{ colspan: 2 }
- %td.notes_content
- .content{ class: ('hide' unless expanded) }
- = render partial: "discussions/notes", collection: discussions, as: :discussion
+- if local_assigns[:on_image]
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
+- else
+ -# Text diff discussions
+ - expanded = local_assigns.fetch(:expanded, true)
+ %tr.notes_holder{ class: ('hide' unless expanded) }
+ %td.notes_line{ colspan: 2 }
+ %td.notes_content
+ .content{ class: ('hide' unless expanded) }
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 4a41be972da..636d06cab53 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -1,18 +1,27 @@
- diff_file = discussion.diff_file
- blob = discussion.blob
+- discussions = { discussion.original_line_code => [discussion] }
+- diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file'
-.diff-file.file-holder
+.diff-file.file-holder{ class: diff_file_class }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
- .diff-content.code.js-syntax-highlight
- %table
- - discussions = { discussion.original_line_code => [discussion] }
- = render partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: diff_file,
- discussions: discussions,
- discussion_expanded: true,
- plain: true }
+ - if diff_file.text?
+ .diff-content.code.js-syntax-highlight
+ %table
+ = render partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: diff_file,
+ discussions: discussions,
+ discussion_expanded: true,
+ plain: true }
+ - else
+ - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
+
+ = render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }
+
+ .note-container
+ = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse: true }
diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml
index cab346fb514..50dd5864195 100644
--- a/app/views/discussions/_new_issue_for_all_discussions.html.haml
+++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml
@@ -1,6 +1,8 @@
- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
.btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
- .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue",
- "aria-label" => "Resolve all discussions in a new issue",
- "data-container" => "body" }
- = link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion'
+ = link_to custom_icon('icon_mr_issue'),
+ new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid),
+ title: 'Resolve all discussions in new issue',
+ aria: { label: 'Resolve all discussions in new issue' },
+ data: { container: 'body' },
+ class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip'
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
index a9bc317b8f8..2bfe118c608 100644
--- a/app/views/discussions/_new_issue_for_discussion.html.haml
+++ b/app/views/discussions/_new_issue_for_discussion.html.haml
@@ -2,7 +2,9 @@
%new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
- .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue",
- "aria-label" => "Resolve this discussion in a new issue",
- "data-container" => "body" }
- = link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion'
+ = link_to custom_icon('icon_mr_issue'),
+ new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
+ title: 'Resolve this discussion in a new issue',
+ aria: { label: 'Resolve this discussion in a new issue' },
+ data: { container: 'body' },
+ class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index db5ab939948..9efcfef690f 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,6 +1,19 @@
-.discussion-notes
- %ul.notes{ data: { discussion_id: discussion.id } }
- = render partial: "shared/notes/note", collection: discussion.notes, as: :note
+- disable_collapse = local_assigns.fetch(:disable_collapse, false)
+- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse
+- badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter]
+- show_toggle = local_assigns.fetch(:show_toggle, true)
+- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
+
+.discussion-notes{ class: collapsed_class }
+ -# Save the first note position data so that we have a reference and can go
+ -# to the first note position when we click on a badge diff discussion
+ %ul.notes{ id: "discussion_#{discussion.id}", data: { discussion_id: discussion.id, position: discussion.notes[0].position.to_json } }
+ - if discussion.try(:on_image?) && show_toggle
+ %button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
+ = sprite_icon('collapse', css_class: 'collapse-icon')
+ %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' }
+ = badge_counter
+ = render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }
.flash-container
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 53ebdd6d2ff..9a763887b30 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -19,8 +19,7 @@
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- - if event.commits_count > 2
- %span ... and #{event.commits_count - 2} more commits.
+ %span ... and #{pluralize(event.commits_count - 1, 'more commit')}.
- if event.md_ref?
- from = event.commit_from
diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg
deleted file mode 100644
index 1522c9d51c9..00000000000
--- a/app/views/feature_highlight/_issue_boards.svg
+++ /dev/null
@@ -1,98 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink">
- <defs>
- <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- </defs>
- <g fill="none" fill-rule="evenodd">
- <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/>
- <g transform="translate(11 23)">
- <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#a)" xlink:href="#b"/>
- <use fill="#F9F9F9" xlink:href="#b"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#c)" xlink:href="#d"/>
- <use fill="#FEF0E8" xlink:href="#d"/>
- <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
- <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
- <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/>
- </g>
- </g>
- <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/>
- <g transform="translate(145 28)">
- <mask id="f" fill="white">
- <use xlink:href="#e"/>
- </mask>
- <use fill="#FFFFFF" xlink:href="#e"/>
- <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#g)" xlink:href="#h"/>
- <use fill="#F9F9F9" xlink:href="#h"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#i)" xlink:href="#j"/>
- <use fill="#FEF0E8" xlink:href="#j"/>
- <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/>
- <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
- <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/>
- </g>
- </g>
- <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/>
- <g transform="translate(78 16)">
- <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#k)" xlink:href="#l"/>
- <use fill="#EFEDF8" xlink:href="#l"/>
- <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
- <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
- <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#m)" xlink:href="#n"/>
- <use fill="#F9F9F9" xlink:href="#n"/>
- </g>
- <g transform="translate(5 74)">
- <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/>
- <use fill="black" filter="url(#o)" xlink:href="#p"/>
- <use fill="#F9F9F9" xlink:href="#p"/>
- </g>
- <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
- </g>
- </g>
-</svg>
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
deleted file mode 100644
index 0f63774fb9b..00000000000
--- a/app/views/groups/_head.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: container_class }
- = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'Group Home' do
- %span
- Home
-
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- %span
- Activity
-
-.hidden-xs
- = render "projects/last_push"
diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml
deleted file mode 100644
index d554bc23743..00000000000
--- a/app/views/groups/_head_issues.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: container_class }
- = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
- = link_to issues_group_path(@group), title: 'List' do
- %span
- List
-
- = nav_link(path: 'labels#index') do
- = link_to group_labels_path(@group), title: 'Labels' do
- %span
- Labels
-
- = nav_link(path: 'milestones#index') do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- %span
- Milestones
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
deleted file mode 100644
index 623d233a46a..00000000000
--- a/app/views/groups/_settings_head.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: container_class }
- = nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: 'General' do
- %span
- General
-
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- %span
- Projects
-
- = nav_link(controller: :ci_cd) do
- = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do
- %span
- Pipelines
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
index 3969e56f937..cb7dab26332 100644
--- a/app/views/groups/activity.html.haml
+++ b/app/views/groups/activity.html.haml
@@ -2,7 +2,6 @@
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
- page_title "Activity"
-= render 'groups/head'
%section.activities
= render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 0d3308833b7..15606dd30fd 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,5 +1,4 @@
- breadcrumb_title "General Settings"
-= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 7f411927429..07e64d9aeaf 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,6 +1,5 @@
- page_title "Issues"
- group_issues_exists = group_issues(@group).exists?
-= render "head_issues"
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 89165096fe2..d10efdad53b 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,8 +1,5 @@
- page_title 'Labels'
-= render "groups/head_issues"
-
-
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml
index d7fabf53587..24eb39b8e2f 100644
--- a/app/views/groups/milestones/_header_title.html.haml
+++ b/app/views/groups/milestones/_header_title.html.haml
@@ -1 +1,2 @@
-- header_title group_title(@group, "Milestones", group_milestones_path(@group))
+- breadcrumb_title @milestone.title
+- add_to_breadcrumbs "Milestones", group_milestones_path(@group)
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index ed582e521c4..cb4fc69d5b8 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,7 +1,5 @@
- page_title "Milestones"
-= render "groups/head_issues"
-
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 7f3f2f707f7..8d2bc810a7d 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,5 +1,4 @@
- breadcrumb_title "Projects"
-= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 9f9ae01e7c5..472da2a6a72 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,5 +1,4 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-= render "groups/settings_head"
= render 'ci/variables/index'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index f4f76887422..3ca63f9c3e0 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,7 +4,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
-= render 'groups/head'
= render 'groups/home_panel'
.groups-header{ class: container_class }
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index 7abc84412c6..869b3b243c6 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -1,7 +1,6 @@
- breadcrumb_title "Details"
- @no_container = true
-= render 'head'
= render 'groups/home_panel'
.groups-header{ class: container_class }
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index c07c148a12a..d6789baea28 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,5 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'help'
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki.prepend-top-default
= markdown @markdown
diff --git a/app/views/layouts/_bootlint.haml b/app/views/layouts/_bootlint.haml
deleted file mode 100644
index d603a74c4e4..00000000000
--- a/app/views/layouts/_bootlint.haml
+++ /dev/null
@@ -1,5 +0,0 @@
--# haml-lint:disable InlineJavaScript
-:javascript
- window.onload = function() {
- var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s);
- }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 34e85fef6d9..f1b32274664 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -32,18 +32,14 @@
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- // TODO: Combine these 2 stylesheets into application.scss
- = stylesheet_link_tag "new_nav", media: "all"
- = stylesheet_link_tag "new_sidebar", media: "all"
-
= Gon::Base.render_data
- if content_for?(:library_javascripts)
= yield :library_javascripts
+ = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") unless I18n.locale == :en
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
- = webpack_bundle_tag "locale"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
@@ -76,4 +72,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
- = render 'layouts/bootlint' if Rails.env.development?
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index cd7a47da4a1..29387d6627e 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -21,8 +21,8 @@
%a
Loading...
= dropdown_loading
- %i.search-icon
- %i.clear-icon.js-clear-input
+ = sprite_icon('search', size: 16, css_class: 'search-icon')
+ = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input')
= hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index d8fc371497d..5ff6ac5fc00 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-gitlab.navbar-gitlab-new
+%header.navbar.navbar-gitlab
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
@@ -22,29 +22,29 @@
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('search')
+ = sprite_icon('search', size: 16)
- if current_user
- %li.user-counter
+ = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('issues')
+ = sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- %li.user-counter
+ = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('mr_bold')
+ = sprite_icon('git-merge', size: 16)
- 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.user-counter
+ = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('todo_done')
+ = sprite_icon('todo-done', size: 16)
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
- = custom_icon('caret_down')
+ = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
@@ -73,7 +73,7 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
- = icon('ellipsis-v', class: 'js-navbar-toggle-right')
- = icon('times', class: 'js-navbar-toggle-left')
+ = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
+ = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 63d1c077ecd..088f2785092 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,7 +1,7 @@
%li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
- = custom_icon('plus_square')
- = custom_icon('caret_down')
+ = sprite_icon('plus-square', size: 16)
+ = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 7bd3f5306a2..002922e13f1 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -16,5 +16,5 @@
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
%li
- %h2.breadcrumbs-sub-title= @breadcrumb_title
+ %h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
= yield :header_content
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index c254ee02dd8..e0d8d9cb402 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -2,7 +2,7 @@
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } }
Projects
- = custom_icon('caret_down')
+ = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
@@ -25,7 +25,7 @@
%li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
- = custom_icon('caret_down')
+ = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu
%ul
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
@@ -54,7 +54,7 @@
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
+ = sprite_icon('admin', size: 18)
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
index 28022eebb19..ad0d51d28f9 100644
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
@@ -4,7 +4,7 @@
%li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= icon("ellipsis-h")
- = icon("angle-right", class: "breadcrumbs-list-angle")
+ = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
.dropdown-menu
%ul
- @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 8ab2b686f86..0ec07605631 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -3,18 +3,17 @@
.context-header
= link_to admin_root_path, title: 'Admin Overview' do
.avatar-container.s40.settings-avatar
- = icon('wrench')
+ = sprite_icon('admin', size: 24)
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
- = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
+ = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
+ = link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container
- = custom_icon('overview')
+ = sprite_icon('overview')
%span.nav-item-name
Overview
-
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
#{ _('Overview') }
@@ -52,16 +51,16 @@
%span
ConvDev Index
- = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
- = sidebar_link admin_conversational_development_index_path, title: _('Monitoring') do
+ = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
+ = link_to admin_system_info_path do
.nav-icon-container
- = custom_icon('monitoring')
+ = sprite_icon('monitor')
%span.nav-item-name
Monitoring
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
- = link_to admin_conversational_development_index_path do
+ = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_system_info_path do
%strong.fly-out-top-item-name
#{ _('Monitoring') }
%li.divider.fly-out-top-item
@@ -87,9 +86,9 @@
Requests Profiles
= nav_link(controller: :broadcast_messages) do
- = sidebar_link admin_broadcast_messages_path, title: _('Messages') do
+ = link_to admin_broadcast_messages_path do
.nav-icon-container
- = custom_icon('messages')
+ = sprite_icon('messages')
%span.nav-item-name
Messages
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -99,9 +98,9 @@
#{ _('Messages') }
= nav_link(controller: [:hooks, :hook_logs]) do
- = sidebar_link admin_hooks_path, title: _('Hooks') do
+ = link_to admin_hooks_path do
.nav-icon-container
- = custom_icon('system_hooks')
+ = sprite_icon('hook')
%span.nav-item-name
System Hooks
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -111,9 +110,9 @@
#{ _('System Hooks') }
= nav_link(controller: :applications) do
- = sidebar_link admin_applications_path, title: _('Applications') do
+ = link_to admin_applications_path do
.nav-icon-container
- = custom_icon('applications')
+ = sprite_icon('applications')
%span.nav-item-name
Applications
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -123,9 +122,9 @@
#{ _('Applications') }
= nav_link(controller: :abuse_reports) do
- = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
+ = link_to admin_abuse_reports_path do
.nav-icon-container
- = custom_icon('abuse_reports')
+ = sprite_icon('slight-frown')
%span.nav-item-name
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
@@ -138,9 +137,9 @@
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
- = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do
+ = link_to admin_spam_logs_path do
.nav-icon-container
- = custom_icon('spam_logs')
+ = sprite_icon('spam')
%span.nav-item-name
Spam Logs
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -150,9 +149,9 @@
#{ _('Spam Logs') }
= nav_link(controller: :deploy_keys) do
- = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
+ = link_to admin_deploy_keys_path do
.nav-icon-container
- = custom_icon('key')
+ = sprite_icon('key')
%span.nav-item-name
Deploy Keys
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -162,9 +161,9 @@
#{ _('Deploy Keys') }
= nav_link(controller: :services) do
- = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
+ = link_to admin_application_settings_services_path do
.nav-icon-container
- = custom_icon('service_templates')
+ = sprite_icon('template')
%span.nav-item-name
Service Templates
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -174,9 +173,9 @@
#{ _('Service Templates') }
= nav_link(controller: :labels) do
- = sidebar_link admin_labels_path, title: _('Labels') do
+ = link_to admin_labels_path do
.nav-icon-container
- = custom_icon('labels')
+ = sprite_icon('labels')
%span.nav-item-name
Labels
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -186,9 +185,9 @@
#{ _('Labels') }
= nav_link(controller: :appearances) do
- = sidebar_link admin_appearances_path, title: _('Appearances') do
+ = link_to admin_appearances_path do
.nav-icon-container
- = custom_icon('appearance')
+ = sprite_icon('appearance')
%span.nav-item-name
Appearance
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -198,9 +197,9 @@
#{ _('Appearance') }
= nav_link(controller: :application_settings) do
- = sidebar_link admin_application_settings_path, title: _('Settings') do
+ = link_to admin_application_settings_path do
.nav-icon-container
- = custom_icon('settings')
+ = sprite_icon('settings')
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items.is-fly-out-only
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index e01dfa7c854..8cba495f7e4 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -11,9 +11,9 @@
= @group.name
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = sidebar_link group_path(@group), title: _('Group overview') do
+ = link_to group_path(@group) do
.nav-icon-container
- = custom_icon('project')
+ = sprite_icon('project')
%span.nav-item-name
Overview
@@ -34,9 +34,9 @@
Activity
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = sidebar_link issues_group_path(@group), title: _('Issues') do
+ = link_to issues_group_path(@group) do
.nav-icon-container
- = custom_icon('issues')
+ = sprite_icon('issues')
%span.nav-item-name
Issues
%span.badge.count= number_with_delimiter(issues.count)
@@ -64,9 +64,9 @@
Milestones
= nav_link(path: 'groups#merge_requests') do
- = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do
+ = link_to merge_requests_group_path(@group) do
.nav-icon-container
- = custom_icon('mr_bold')
+ = sprite_icon('git-merge')
%span.nav-item-name
Merge Requests
%span.badge.count= number_with_delimiter(merge_requests.count)
@@ -77,21 +77,21 @@
#{ _('Merge Requests') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
- = sidebar_link group_group_members_path(@group), title: _('Members') do
+ = link_to group_group_members_path(@group) do
.nav-icon-container
- = custom_icon('members')
+ = sprite_icon('users')
%span.nav-item-name
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
+ = link_to group_group_members_path(@group) do
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = sidebar_link edit_group_path(@group), title: _('Settings') do
+ = link_to edit_group_path(@group) do
.nav-icon-container
- = custom_icon('settings')
+ = sprite_icon('settings')
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 4c26d107ea7..458b5010d36 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -3,13 +3,13 @@
.context-header
= link_to profile_path, title: 'Profile Settings' do
.avatar-container.s40.settings-avatar
- = icon('user')
+ = sprite_icon('user', size: 24)
.sidebar-context-title User Settings
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = sidebar_link profile_path, title: _('Profile Settings') do
+ = link_to profile_path do
.nav-icon-container
- = custom_icon('profile')
+ = sprite_icon('profile')
%span.nav-item-name
Profile
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -18,9 +18,9 @@
%strong.fly-out-top-item-name
#{ _('Profile') }
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = sidebar_link profile_account_path, title: _('Account') do
+ = link_to profile_account_path do
.nav-icon-container
- = custom_icon('account')
+ = sprite_icon('account')
%span.nav-item-name
Account
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -30,9 +30,9 @@
#{ _('Account') }
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
- = sidebar_link applications_profile_path, title: _('Applications') do
+ = link_to applications_profile_path do
.nav-icon-container
- = custom_icon('applications')
+ = sprite_icon('applications')
%span.nav-item-name
Applications
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -41,9 +41,9 @@
%strong.fly-out-top-item-name
#{ _('Applications') }
= nav_link(controller: :chat_names) do
- = sidebar_link profile_chat_names_path, title: _('Chat') do
+ = link_to profile_chat_names_path do
.nav-icon-container
- = custom_icon('chat')
+ = sprite_icon('comment')
%span.nav-item-name
Chat
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -52,9 +52,9 @@
%strong.fly-out-top-item-name
#{ _('Chat') }
= nav_link(controller: :personal_access_tokens) do
- = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
+ = link_to profile_personal_access_tokens_path do
.nav-icon-container
- = custom_icon('access_tokens')
+ = sprite_icon('token')
%span.nav-item-name
Access Tokens
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -63,9 +63,9 @@
%strong.fly-out-top-item-name
#{ _('Access Tokens') }
= nav_link(controller: :emails) do
- = sidebar_link profile_emails_path, title: _('Emails') do
+ = link_to profile_emails_path do
.nav-icon-container
- = custom_icon('emails')
+ = sprite_icon('mail')
%span.nav-item-name
Emails
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -75,9 +75,9 @@
#{ _('Emails') }
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = sidebar_link edit_profile_password_path, title: _('Password') do
+ = link_to edit_profile_password_path do
.nav-icon-container
- = custom_icon('lock')
+ = sprite_icon('lock')
%span.nav-item-name
Password
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -86,9 +86,9 @@
%strong.fly-out-top-item-name
#{ _('Password') }
= nav_link(controller: :notifications) do
- = sidebar_link profile_notifications_path, title: _('Notifications') do
+ = link_to profile_notifications_path do
.nav-icon-container
- = custom_icon('notifications')
+ = sprite_icon('notifications')
%span.nav-item-name
Notifications
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -97,9 +97,9 @@
%strong.fly-out-top-item-name
#{ _('Notifications') }
= nav_link(controller: :keys) do
- = sidebar_link profile_keys_path, title: _('SSH Keys') do
+ = link_to profile_keys_path do
.nav-icon-container
- = custom_icon('key')
+ = sprite_icon('key')
%span.nav-item-name
SSH Keys
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -108,9 +108,9 @@
%strong.fly-out-top-item-name
#{ _('SSH Keys') }
= nav_link(controller: :gpg_keys) do
- = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
+ = link_to profile_gpg_keys_path do
.nav-icon-container
- = custom_icon('key_2')
+ = sprite_icon('key-2')
%span.nav-item-name
GPG Keys
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -119,9 +119,9 @@
%strong.fly-out-top-item-name
#{ _('GPG Keys') }
= nav_link(controller: :preferences) do
- = sidebar_link profile_preferences_path, title: _('Preferences') do
+ = link_to profile_preferences_path do
.nav-icon-container
- = custom_icon('preferences')
+ = sprite_icon('preferences')
%span.nav-item-name
Preferences
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -130,9 +130,9 @@
%strong.fly-out-top-item-name
#{ _('Preferences') }
= nav_link(path: 'profiles#audit_log') do
- = sidebar_link audit_log_profile_path, title: _('Authentication log') do
+ = link_to audit_log_profile_path do
.nav-icon-container
- = custom_icon('authentication_log')
+ = sprite_icon('log')
%span.nav-item-name
Authentication log
%ul.sidebar-sub-level-items.is-fly-out-only
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 27fadc1d952..759d6ff68ea 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -9,9 +9,9 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
+ = link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
- = custom_icon('project')
+ = sprite_icon('project')
%span.nav-item-name
Overview
@@ -36,9 +36,9 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
- = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do
+ = link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
- = custom_icon('doc_text')
+ = sprite_icon('doc_text')
%span.nav-item-name
Repository
@@ -82,17 +82,17 @@
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
- = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
- = custom_icon('container_registry')
+ = sprite_icon('disk')
%span.nav-item-name
Registry
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do
+ = link_to project_issues_path(@project), class: 'shortcuts-issues' do
.nav-icon-container
- = custom_icon('issues')
+ = sprite_icon('issues')
%span.nav-item-name
Issues
- if @project.issues_enabled?
@@ -108,29 +108,15 @@
%span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count)
%li.divider.fly-out-top-item
- = nav_link(controller: :issues) do
+ = nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
List
= nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: 'Board' do
+ = link_to project_boards_path(@project), title: boards_link_text do
%span
- Board
- .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
- .feature-highlight-popover-content
- = render 'feature_highlight/issue_boards.svg'
- .feature-highlight-popover-sub-content
- %span= _('Use')
- = link_to 'Issue Boards', project_boards_path(@project)
- %span= _('to create customized software development workflows like')
- %strong= _('Scrum')
- %span= _('or')
- %strong= _('Kanban')
- %hr
- %button.btn-link.dismiss-feature-highlight{ type: 'button' }
- %span= _("Got it! Don't show this again")
- = custom_icon('thumbs_up')
+ = boards_link_text
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
@@ -144,9 +130,9 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do
.nav-icon-container
- = custom_icon('mr_bold')
+ = sprite_icon('git-merge')
%span.nav-item-name
Merge Requests
%span.badge.count.merge_counter.js-merge-counter
@@ -160,10 +146,10 @@
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
- = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters]) do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
- = custom_icon('pipeline')
+ = sprite_icon('pipeline')
%span.nav-item-name
CI / CD
@@ -203,11 +189,17 @@
%span
Charts
+ - if project_nav_tab? :clusters
+ = nav_link(controller: :clusters) do
+ = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
+ %span
+ Cluster
+
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
- = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
+ = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
.nav-icon-container
- = custom_icon('wiki')
+ = sprite_icon('book')
%span.nav-item-name
Wiki
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -218,9 +210,9 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
- = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
+ = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
.nav-icon-container
- = custom_icon('snippets')
+ = sprite_icon('snippet')
%span.nav-item-name
Snippets
%ul.sidebar-sub-level-items.is-fly-out-only
@@ -231,9 +223,9 @@
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
- = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do
+ = link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
- = custom_icon('settings')
+ = sprite_icon('settings')
%span.nav-item-name
Settings
@@ -277,7 +269,7 @@
= nav_link(path: %w[members#show]) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
.nav-icon-container
- = custom_icon('members')
+ = sprite_icon('users')
%span.nav-item-name
Members
diff --git a/app/views/notify/new_email_email.html.haml b/app/views/notify/new_email_email.html.haml
deleted file mode 100644
index 4a0448a573c..00000000000
--- a/app/views/notify/new_email_email.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%p
- Hi #{@user.name}!
-%p
- A new email was added to your account:
-%p
- email:
- %code= @email.email
-%p
- If this email was added in error, you can remove it here:
- = link_to "Emails", profile_emails_url
diff --git a/app/views/notify/new_email_email.text.erb b/app/views/notify/new_email_email.text.erb
deleted file mode 100644
index 51cba99ad0d..00000000000
--- a/app/views/notify/new_email_email.text.erb
+++ /dev/null
@@ -1,7 +0,0 @@
-Hi <%= @user.name %>!
-
-A new email was added to your account:
-
-email.................. <%= @email.email %>
-
-If this email was added in error, you can remove it here: <%= profile_emails_url %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 5d778d67ae7..7f79168dfb3 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -74,7 +74,9 @@
%h4.prepend-top-0.warning-title
Change username
%p
- Changing your username will change path to all personal projects!
+ Changing your username can have unintended side effects.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
@@ -95,21 +97,29 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.danger-title
- Remove account
+ = s_('Profiles|Delete account')
.col-lg-8
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
- Deleting an account has the following effects:
+ = s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+
+ #delete-account-modal{ data: { action_url: user_registration_path,
+ confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
+ username: current_user.username } }
+ %button.btn.btn-danger.disabled
+ = s_('Profiles|Delete account')
- else
- if @user.solo_owned_groups.present?
%p
- Your account is currently an owner in these groups:
+ = s_('Profiles|Your account is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
- You must transfer ownership or delete these groups before you can delete your account.
+ = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.')
- else
%p
- You don't have access to delete this user.
+ = s_("Profiles|You don't have access to delete this user.")
.append-bottom-default
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('account')
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 612ecbbb96a..df1df4f5d72 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -32,19 +32,25 @@
All email addresses will be used to identify your commits.
%ul.well-list
%li
- = @primary
+ = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.pull-right
%span.label.label-success Primary email
- - if @primary === current_user.public_email
+ - if @primary_email === current_user.public_email
%span.label.label-info Public email
- - if @primary === current_user.notification_email
+ - if @primary_email === current_user.notification_email
%span.label.label-info Notification email
- @emails.each do |email|
%li
- = email.email
+ = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.pull-right
- if email.email === current_user.public_email
%span.label.label-info Public email
- if email.email === current_user.notification_email
%span.label.label-info Notification email
- = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10'
+ - unless email.confirmed?
+ - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
+ = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
+
+ = link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do
+ %span.sr-only Remove
+ = icon('trash')
diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/profiles/gpg_keys/_email_with_badge.html.haml
deleted file mode 100644
index 5f7844584e1..00000000000
--- a/app/views/profiles/gpg_keys/_email_with_badge.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- css_classes = %w(label label-verification-status)
-- css_classes << (verified ? 'verified': 'unverified')
-- text = verified ? 'Verified' : 'Unverified'
-
-.gpg-email-badge
- .gpg-email-badge-email= email
- %div{ class: css_classes }
- = text
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index b04981f90e3..5ed517c1ef6 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -3,10 +3,17 @@
= icon 'key', class: "settings-list-icon hidden-xs"
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
- = render partial: 'email_with_badge', locals: { email: email, verified: verified }
+ = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
.description
%code= key.fingerprint
+ - if key.subkeys.present?
+ .subkeys
+ %span.bold Subkeys:
+ %ul.subkeys-list
+ - key.subkeys.each do |subkey|
+ %li
+ %code= subkey.fingerprint
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 2b72eeab8d6..66d1d1e8d44 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -17,7 +17,7 @@
.preview-row
.quadrant.three
.quadrant.four
- = f.radio_button :theme_id, theme.id
+ = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
.col-sm-12
@@ -36,10 +36,10 @@
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id
= scheme.name
+
.col-sm-12
%hr
- .col-sm-12
- %hr
+
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Behavior
diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml
deleted file mode 100644
index dba84838a52..00000000000
--- a/app/views/projects/_head.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: container_class }
- = nav_link(path: 'projects#show') do
- = link_to project_path(@project), title: _('Project home'), class: 'shortcuts-project' do
- %span= _('Home')
-
- = nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
- %span= _('Activity')
-
- - if can?(current_user, :read_cycle_analytics, @project)
- = nav_link(path: 'cycle_analytics#show') do
- = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
- %span= _('Cycle Analytics')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 873b3045ea9..619b632918e 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,4 +1,6 @@
- empty_repo = @project.empty_repo?
+- fork_network = @project.fork_network
+- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
@@ -12,11 +14,15 @@
- if @project.description.present?
= markdown_field(@project, :description)
- - if forked_from_project = @project.forked_from_project
+ - if @project.forked?
%p
- #{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(forked_from_project) do
- = forked_from_project.namespace.try(:name)
+ - if forked_from_project
+ #{ s_('ForkedFromProjectPath|Forked from') }
+ = link_to project_path(forked_from_project) do
+ = forked_from_project.full_name
+ - else
+ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
+ = deleted_message % { project_name: fork_network.deleted_root_project_name }
.project-repo-buttons
.count-buttons
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 71424593f2e..770608eddff 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,5 +1,12 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
+- if defined?(@merge_request) && @merge_request.discussion_locked?
+ .issuable-note-warning
+ = icon('lock', class: 'icon')
+ %span
+ = _('This merge request is locked.')
+ = _('Only project members can comment.')
+
.md-area
.md-header
%ul.nav-links.clearfix
diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml
new file mode 100644
index 00000000000..9d357293a2f
--- /dev/null
+++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml
@@ -0,0 +1,13 @@
+- form = local_assigns.fetch(:form)
+- project = local_assigns.fetch(:project)
+
+.radio
+ = label_tag :project_merge_method_ff do
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
+ %strong Fast-forward merge
+ %br
+ %span.descr
+ No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
+ %br
+ %span.descr
+ When fast-forward merge is not possible, the user must first rebase locally.
diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml
new file mode 100644
index 00000000000..c52e09573a6
--- /dev/null
+++ b/app/views/projects/_merge_request_rebase_settings.html.haml
@@ -0,0 +1,13 @@
+- form = local_assigns.fetch(:form)
+
+.radio
+ = label_tag :project_merge_method_rebase_merge do
+ = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
+ %strong Merge commit with semi-linear history
+ %br
+ %span.descr
+ A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
+ This way you could make sure that if this merge request would build, after merging to target branch it would also build.
+ %br
+ %span.descr
+ When fast-forward merge is not possible, the user must first rebase locally.
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index cc5afa943cf..fd0c419cdac 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,3 +1,18 @@
- form = local_assigns.fetch(:form)
+.form-group
+ = label_tag :merge_method_merge, class: 'label-light' do
+ Merge method
+ .radio
+ = label_tag :project_merge_method_merge do
+ = form.radio_button :merge_method, :merge, class: "js-merge-method-radio"
+ %strong Merge commit
+ %br
+ %span.descr
+ A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
+
+ = render 'merge_request_rebase_settings', form: form
+
+ = render 'merge_request_fast_forward_settings', project: @project, form: form
+
= render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index f80dadb8037..d0ab39033cf 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -2,8 +2,6 @@
- page_title _("Activity")
-= render "projects/head"
-
= render 'projects/last_push'
= render 'projects/activity'
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index 28f7a52df25..a97ddb3c377 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,13 +1,17 @@
-- path_to_file = file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: file.path) if @build.downloadable_single_artifacts_file?
+- blob = file.blob
+- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
+- external_link = blob.external_link?(@build)
-%tr.tree-item{ 'data-link' => path_to_file }
- - blob = file.blob
+%tr.tree-item.js-artifact-tree-row{ data: { link: path_to_file, external_link: "#{external_link}" } }
%td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name)
- %span.str-truncated
- - if path_to_file
- = link_to file.name, path_to_file
- - else
- = file.name
+ - if external_link
+ = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip',
+ target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do
+ %span.str-truncated>= blob.name
+ = icon('external-link', class: 'js-artifact-tree-external-icon')
+ - else
+ = link_to path_to_file do
+ %span.str-truncated= blob.name
%td
= number_to_human_size(blob.size, precision: 2)
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 4cc3218d967..fe02cbcbf95 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,10 +1,9 @@
- breadcrumb_title _('Artifacts')
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
-= render "projects/pipelines/head"
= render "projects/jobs/header", show_controls: false
-- add_to_breadcrumbs(_('Jobs'), project_jobs_path(@project))
+- add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project))
- add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project))
.tree-holder
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index b85bbcb980e..2942d618a42 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -1,5 +1,4 @@
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
-= render "projects/pipelines/head"
= render "projects/jobs/header", show_controls: false
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 60ac202bde0..e45861ac08d 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- project_duration = age_map_duration(@blame_groups, @project)
- page_title "Blame", @blob.path, @ref
-= render "projects/commits/head"
%div{ class: container_class }
#blob-content-holder.tree-holder
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index d1d448f0d4c..ea7a71792a3 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -5,25 +5,24 @@
= diff_match_line @form.since, @form.since, text: @match_line, view: diff_view
- @lines.each_with_index do |line, index|
- - line_new = index + @form.since
- - line_old = line_new - @form.offset
- - line_content = capture do
- %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line}
- %tr.line_holder.diff-expanded{ id: line_old, class: line_class }
+ - line_number_new = index + @form.since
+ - line_number_old = line_number_new - @form.offset
+ - line[0, 0] = ' ' * @form.indent
+ %tr.line_holder.diff-expanded{ id: line_number_old, class: line_class }
- case diff_view
- when :inline
- %td.old_line.diff-line-num{ data: { linenumber: line_old } }
- %a{ href: "#", data: { linenumber: line_old }, disabled: true }
- %td.new_line.diff-line-num{ data: { linenumber: line_new } }
- %a{ href: "#", data: { linenumber: line_new }, disabled: true }
- = line_content
+ %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
+ %a{ href: "#", data: { linenumber: line_number_old }, disabled: true }
+ %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
+ %a{ href: "#", data: { linenumber: line_number_new }, disabled: true }
+ %td.line_content.noteable_line{ class: line_class }= line
- when :parallel
- %td.old_line.diff-line-num{ data: { linenumber: line_old } }
- %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true }
- = line_content
- %td.new_line.diff-line-num{ data: { linenumber: line_new } }
- %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true }
- = line_content
+ %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
+ %a{ href: "##{line_number_old}", data: { linenumber: line_number_old }, disabled: true }
+ %td.line_content.noteable_line.left-side{ class: line_class }= line
+ %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
+ %a{ href: "##{line_number_new}", data: { linenumber: line_number_new }, disabled: true }
+ %td.line_content.noteable_line.right-side{ class: line_class }= line
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
%tr.line_holder{ id: @form.to, class: line_class }
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 992fe7f717f..626cbc9e41d 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob')
-= render "projects/commits/head"
%div{ class: container_class }
- if @conflict
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 240e62d5ac5..c4712bf3736 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -2,7 +2,6 @@
- @no_container = true
- page_title @blob.path, @ref
-= render "projects/commits/head"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'blob'
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index 6d1138f7959..253566c43be 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -1,5 +1,5 @@
.file-content.blob_file.blob-no-preview
- .center
+ .center.render-error.vertical-center
= link_to blob_raw_path do
%h1.light
= icon('download')
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index d0fcd55f6c1..6d6bd79bc3c 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index 2318cf82f58..a5f73fb0197 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= icon('spinner spin fw')
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19712a8f1be..49101d1efa4 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -13,20 +13,23 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
- merged
+ %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ = s_('Branches|merged')
- if protected_branch?(@project, branch)
%span.label.label-success
- protected
+ = s_('Branches|protected')
.controls.hidden-xs<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
- Merge request
+ = _('Merge request')
- if branch.name != @repository.root_ref
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
- Compare
+ = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
+ class: "btn btn-default #{'prepend-left-10' unless merge_project}",
+ method: :post,
+ title: s_('Branches|Compare') do
+ = s_('Branches|Compare')
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
@@ -34,34 +37,37 @@
- if branch.name == @project.repository.root_ref
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: "The default branch cannot be deleted" }
+ title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
- elsif protected_branch?(@project, branch)
- if can?(current_user, :delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: "Delete protected branch",
+ title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
- branch_name: branch.name } }
+ branch_name: branch.name,
+ is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } }
= icon("trash-o")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: "Only a project master or owner can delete a protected branch" }
+ title: s_('Branches|Only a project master or owner can delete a protected branch') }
= icon("trash-o")
- else
= link_to project_branch_path(@project, branch.name),
class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: "Delete branch",
+ title: s_('Branches|Delete branch'),
method: :delete,
- data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
+ data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
remote: true,
- "aria-label" => "Delete branch" do
+ 'aria-label' => s_('Branches|Delete branch') do
= icon("trash-o")
- if branch.name != @repository.root_ref
- .divergence-graph{ title: "#{number_commits_behind} commits behind #{@repository.root_ref}, #{number_commits_ahead} commits ahead" }
+ .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind,
+ default_branch: @repository.root_ref,
+ number_commits_ahead: number_commits_ahead } }
.graph-side
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
%span.count.count-behind= number_commits_behind
@@ -75,4 +81,4 @@
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
- Cant find HEAD commit for this branch
+ = s_('Branches|Cant find HEAD commit for this branch')
diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml
index c5888afa54d..e0008e322a0 100644
--- a/app/views/projects/branches/_delete_protected_modal.html.haml
+++ b/app/views/projects/branches/_delete_protected_modal.html.haml
@@ -4,31 +4,38 @@
.modal-header
%button.close{ data: { dismiss: 'modal' } } ×
%h3.page-title
- Delete protected branch
- = surround "'", "'?" do
- %span.js-branch-name>[branch name]
+ - title_branch_name = capture do
+ %span.js-branch-name.ref-name>[branch name]
+ = s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
.modal-body
%p
- You’re about to permanently delete the protected branch
- = succeed '.' do
- %strong.js-branch-name [branch name]
+ - branch_name = capture do
+ %strong.js-branch-name.ref-name>[branch name]
+ = s_('Branches|You’re about to permanently delete the protected branch %{branch_name}.').html_safe % { branch_name: branch_name }
+ %p.js-not-merged
+ - default_branch = capture do
+ %span.ref-name= @repository.root_ref
+ = s_('Branches|This branch hasn’t been merged into %{default_branch}.').html_safe % { default_branch: default_branch }
+ = s_('Branches|To avoid data loss, consider merging this branch before deleting it.')
%p
- Once you confirm and press
- = succeed ',' do
- %strong Delete protected branch
- it cannot be undone or recovered.
+ - delete_protected_branch = capture do
+ %strong
+ = s_('Branches|Delete protected branch')
+ = s_('Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered.').html_safe % { delete_protected_branch: delete_protected_branch }
%p
- %strong To confirm, type
- %kbd.js-branch-name [branch name]
+ - branch_name_confirmation = capture do
+ %kbd.js-branch-name [branch name]
+ %strong
+ = s_('Branches|To confirm, type %{branch_name_confirmation}:').html_safe % { branch_name_confirmation: branch_name_confirmation }
.form-group
= text_field_tag 'delete_branch_input', '', class: 'form-control js-delete-branch-input'
.modal-footer
%button.btn{ data: { dismiss: 'modal' } } Cancel
- = link_to 'Delete protected branch', '',
+ = link_to s_('Branches|Delete protected branch'), '',
class: "btn btn-danger js-delete-branch",
- title: 'Delete branch',
+ title: s_('Branches|Delete branch'),
method: :delete,
- "aria-label" => "Delete"
+ 'aria-label' => s_('Branches|Delete branch')
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 73583c6bbc2..7d9645d79e6 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,17 +1,16 @@
- @no_container = true
-- page_title "Branches"
-= render "projects/commits/head"
+- page_title _('Branches')
%div{ class: container_class }
.top-area.adjust
- if can?(current_user, :admin_project, @project)
.nav-text
- Protected branches can be managed in
- = link_to 'project settings', project_protected_branches_path(@project)
+ - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
+ = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link }
.nav-controls
= form_tag(filter_branches_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline>
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
@@ -20,16 +19,21 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
- Sort by
+ = s_('Branches|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 project_merged_branches_path(@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
- Delete merged branches
+ = link_to project_merged_branches_path(@project),
+ class: 'btn btn-inverted btn-remove has-tooltip',
+ title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref },
+ method: :delete,
+ data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'),
+ container: 'body' } do
+ = s_('Branches|Delete merged branches')
= link_to new_project_branch_path(@project), class: 'btn btn-create' do
- New branch
+ = s_('Branches|New branch')
- if @branches.any?
%ul.content-list.all-branches
@@ -37,6 +41,7 @@
= render "projects/branches/branch", branch: branch
= paginate @branches, theme: 'gitlab'
- else
- .nothing-here-block No branches to show
+ .nothing-here-block
+ = s_('Branches|No branches to show')
= render 'projects/branches/delete_protected_modal'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 883922dbf04..fa6f6d0b588 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
-- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
@@ -11,33 +11,26 @@
#{ _('Source code') }
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download zip')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar.gz')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar.bz2')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar')
- - if pipeline
- - artifacts = pipeline.builds.latest.with_artifacts
- - if artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - artifacts.each do |job|
- %li
- = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %i.fa.fa-download
- %span
- #{ s_('DownloadArtifacts|Download') } '#{job.name}'
+ - if pipeline && pipeline.latest_builds_with_artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
+ %span
+ #{s_('DownloadArtifacts|Download')} '#{job.name}'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index b04d6a1fa5e..2589c53beae 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -11,19 +11,16 @@
- if can_create_issue
%li
= link_to new_project_issue_path(@project) do
- = icon('exclamation-circle fw')
#{ _('New issue') }
- if merge_project
%li
= link_to project_new_merge_request_path(merge_project) do
- = icon('tasks fw')
#{ _('New merge request') }
- if can_create_snippet
%li
= link_to new_project_snippet_path(@project) do
- = icon('file-text-o fw')
#{ _('New snippet') }
- if can_create_issue || merge_project || can_create_snippet
@@ -32,20 +29,16 @@
- if can?(current_user, :push_code, @project)
%li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do
- = icon('file fw')
#{ _('New file') }
%li
= link_to new_project_branch_path(@project) do
- = icon('code-fork fw')
#{ _('New branch') }
%li
= link_to new_project_tag_path(@project) do
- = icon('tags fw')
#{ _('New tag') }
- elsif current_user && current_user.already_forked?(@project)
%li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do
- = icon('file fw')
#{ _('New file') }
- elsif can?(current_user, :fork_project, @project)
%li
@@ -55,5 +48,4 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('file fw')
#{ _('New file') }
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index f45cc7f0f45..f880556a9f7 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -4,12 +4,11 @@
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do
= custom_icon('icon_fork')
%span= s_('GoToYourFork|Fork')
- - elsif !current_user.can_create_project?
- = link_to new_project_fork_path(@project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do
- = custom_icon('icon_fork')
- %span= s_('CreateNewFork|Fork')
- else
- = link_to new_project_fork_path(@project), class: 'btn' do
+ - can_create_fork = current_user.can?(:create_fork)
+ = link_to new_project_fork_path(@project),
+ class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}",
+ title: (_('You have reached your project limit') unless can_create_fork) do
= custom_icon('icon_fork')
%span= s_('CreateNewFork|Fork')
.count-with-arrow
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
new file mode 100644
index 00000000000..371cdb1e403
--- /dev/null
+++ b/app/views/projects/clusters/_form.html.haml
@@ -0,0 +1,37 @@
+.row
+ .col-sm-8.col-sm-offset-4
+ %p
+ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
+
+ = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :gcp_cluster_name, class: 'form-control'
+
+ .form-group
+ = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_project_id, class: 'form-control'
+
+ .form-group
+ = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+
+ .form-group
+ = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
+ = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+
+ .form-group
+ = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+
+ .form-group
+ = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/_header.html.haml
new file mode 100644
index 00000000000..0134d46491c
--- /dev/null
+++ b/app/views/projects/clusters/_header.html.haml
@@ -0,0 +1,14 @@
+%h4.prepend-top-0
+ = s_('ClusterIntegration|Create new cluster on Google Container Engine')
+%p
+ = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
+%ul
+ %li
+ - link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine }
+ %li
+ - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
+ %li
+ - link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|A %{link_to_container_project} must have been created under this account').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
new file mode 100644
index 00000000000..761879db32b
--- /dev/null
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -0,0 +1,7 @@
+%h4.prepend-top-0
+ = s_('ClusterIntegration|Cluster integration')
+%p
+ = s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+%p
+ - link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/login.html.haml
new file mode 100644
index 00000000000..ae132672b7e
--- /dev/null
+++ b/app/views/projects/clusters/login.html.haml
@@ -0,0 +1,16 @@
+- breadcrumb_title "Cluster"
+- page_title _("Login")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+.row
+ .col-sm-8.col-sm-offset-4.signin-with-google
+ - if @authorize_url
+ = link_to @authorize_url do
+ = image_tag('auth_buttons/signin_with_google.png')
+ - else
+ - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
+ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
new file mode 100644
index 00000000000..c538d41ffad
--- /dev/null
+++ b/app/views/projects/clusters/new.html.haml
@@ -0,0 +1,9 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
new file mode 100644
index 00000000000..aee6f904a62
--- /dev/null
+++ b/app/views/projects/clusters/show.html.haml
@@ -0,0 +1,70 @@
+- breadcrumb_title "Cluster"
+- page_title _("Cluster")
+
+- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
+.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
+ toggle_status: @cluster.enabled? ? 'true': 'false',
+ cluster_status: @cluster.status_name,
+ cluster_status_reason: @cluster.status_reason } }
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ %label.append-bottom-10{ for: 'enable-cluster-integration' }
+ = s_('ClusterIntegration|Enable cluster integration')
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+
+ = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_errors(@cluster)
+ .form-group.append-bottom-20
+ %label.append-bottom-10
+ = field.hidden_field :enabled, { class: 'js-toggle-input'}
+
+ %button{ type: 'button',
+ class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
+ disabled: !can?(current_user, :update_cluster, @cluster),
+ data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
+
+ - if can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
+
+ - if can?(current_user, :admin_cluster, @cluster)
+ %label.append-bottom-10{ for: 'google-container-engine' }
+ = s_('ClusterIntegration|Google Container Engine')
+ %p
+ - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+
+ .hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ %p.js-error-reason
+
+ .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+
+ .hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
+
+ .form_group.append-bottom-20
+ %label.append-bottom-10{ for: 'cluter-name' }
+ = s_('ClusterIntegration|Cluster name')
+ .input-group
+ %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
+ %span.input-group-addon.clipboard-addon
+ = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
+
+ - if can?(current_user, :admin_cluster, @cluster)
+ .well.form_group
+ %label.text-danger
+ = s_('ClusterIntegration|Remove cluster integration')
+ %p
+ = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.')
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index bef96786b73..49b0b314e1d 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -3,6 +3,8 @@
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
+ "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
+ "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 717de85c5d2..abb292f8f27 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -6,7 +6,6 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
-= render "projects/commits/head"
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index b8655808d89..a16ffb433a5 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -32,7 +32,7 @@
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- - commit_timeago = time_ago_with_tooltip(commit.committed_date)
+ - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
deleted file mode 100644
index e1549baef89..00000000000
--- a/app/views/projects/commits/_head.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
- = link_to project_tree_path(@project) do
- #{ _('Files') }
-
- = nav_link(controller: [:commit, :commits]) do
- = link_to project_commits_path(@project, current_ref) do
- #{ _('Commits') }
-
- = nav_link(html_options: {class: branches_tab_class}) do
- = link_to project_branches_path(@project) do
- #{ _('Branches') }
-
- = nav_link(controller: [:tags, :releases]) do
- = link_to project_tags_path(@project) do
- #{ _('Tags') }
-
- = nav_link(path: 'graphs#show') do
- = link_to project_graph_path(@project, current_ref) do
- #{ _('Contributors') }
-
- = nav_link(controller: %w(network)) do
- = link_to project_network_path(@project, current_ref) do
- #{ s_('ProjectNetworkGraph|Graph') }
-
- = nav_link(controller: :compare) do
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
- #{ _('Compare') }
-
- = nav_link(path: 'graphs#charts') do
- = link_to charts_project_graph_path(@project, current_ref) do
- #{ _('Charts') }
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index e873b931683..ef305120525 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -5,9 +5,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
-= content_for :sub_nav do
- = render "head"
-
.js-project-commits-show{ 'data-commits-limit' => @limit }
%div{ class: container_class }
.tree-holder
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 94b7db5eb25..a518fced2b4 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -2,22 +2,22 @@
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'}
- .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
- .input-group.inline-input-group
- %span.input-group-addon from
- = hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
- = render 'shared/ref_dropdown'
- .compare-ellipsis.inline ...
+ = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
- %span.input-group-addon to
+ %span.input-group-addon Source
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render 'shared/ref_dropdown'
+ .compare-ellipsis.inline ...
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
+ .input-group.inline-input-group
+ %span.input-group-addon Target
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
+ = render 'shared/ref_dropdown'
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- if @merge_request.present?
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 2632fea6eba..3ad0166e9cd 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,19 +1,24 @@
- @no_container = true
- breadcrumb_title "Compare Revisions"
- page_title "Compare"
-= render "projects/commits/head"
%div{ class: container_class }
.sub-header-block
Compare Git revisions.
%br
- Fill input field with commit SHA like
- %code.ref-name 4eedf23
- or branch/tag name like
- %code.ref-name master
- and press compare button for the commits list and a code diff.
+ Choose a branch/tag (e.g.
+ = succeed ')' do
+ %code.ref-name master
+ or enter a commit SHA (e.g.
+ = succeed ')' do
+ %code.ref-name 4eedf23
+ to see what's changed or to create a merge request.
%br
- Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
+ Changes are shown as if the
+ %b source
+ revision was being merged into the
+ %b target
+ revision.
.prepend-top-20
= render "form"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 7cc42455394..f87f1d476f5 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project)
- page_title "#{params[:from]}...#{params[:to]}"
-= render "projects/commits/head"
%div{ class: container_class }
.sub-header-block.no-bottom-space
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 8d008be5aae..71d30da14a9 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -4,22 +4,11 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('cycle_analytics')
-= render "projects/head"
-
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
- .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" }
- %button.dismiss-button{ type: 'button', 'aria-label': 'Dismiss Cycle Analytics introduction box', "@click" => "dismissOverviewDialog()" }
- = icon("times")
- .svg-container
- = custom_icon('icon_cycle_analytics_splash')
- .inner-content
- %h4
- {{ __('Introducing Cycle Analytics') }}
- %p
- {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
- %p
- = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+ %banner{ "v-if" => "!isOverviewDialogDismissed",
+ "documentation-link": help_page_path('user/project/cycle_analytics'),
+ "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
= icon("spinner spin", "v-show" => "isLoading")
.wrapper{ "v-show" => "!isLoading && !hasError" }
.panel.panel-default
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index b5aea217384..adc4dcbed33 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,5 +1,8 @@
- environment = local_assigns.fetch(:environment, nil)
- file_hash = hexdigest(diff_file.file_path)
+- image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
+- image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
+
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) }
.js-file-title.file-title-flex-parent
.file-header-content
@@ -17,6 +20,9 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
+ - if image_diff && image_replaced
+ = view_file_button(diff_file.old_content_sha, diff_file.old_path, project, replaced: true)
+
= view_file_button(diff_file.content_sha, diff_file.file_path, project)
= view_on_environment_button(diff_file.content_sha, diff_file.file_path, environment) if environment
diff --git a/app/views/projects/diffs/_image_diff_frame.html.haml b/app/views/projects/diffs/_image_diff_frame.html.haml
new file mode 100644
index 00000000000..dae73e10460
--- /dev/null
+++ b/app/views/projects/diffs/_image_diff_frame.html.haml
@@ -0,0 +1,5 @@
+- class_name = local_assigns.fetch(:class_name, '')
+- note_type = local_assigns.fetch(:note_type, '')
+
+.frame{ class: class_name, data: { position: position, note_type: note_type } }
+ = image_tag(image_path, alt: alt, draggable: false, lazy: false)
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 56d63250714..1f0ca211074 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -14,20 +14,20 @@
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
- %td.line_content.match= left.text
+ %td.line_content.match.left-side= left.text
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
- %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ %td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id }
- %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text)
+ %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
- %td.line_content.parallel
+ %td.line_content.parallel.left-side
- if right
- case right.type
@@ -35,20 +35,20 @@
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num
- %td.line_content.match= right.text
+ %td.line_content.match.right-side= right.text
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
- %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ %td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id }
- %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text)
+ %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
- %td.line_content.parallel
+ %td.line_content.parallel.right-side
- if discussions_left || discussions_right
= render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right
diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml
new file mode 100644
index 00000000000..8fc232b464e
--- /dev/null
+++ b/app/views/projects/diffs/_replaced_image_diff.html.haml
@@ -0,0 +1,61 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- click_to_comment = local_assigns.fetch(:click_to_comment, true)
+- diff_view_data = local_assigns.fetch(:diff_view_data, '')
+- class_name = ''
+
+- if click_to_comment
+ - class_name = 'js-add-image-diff-note-button click-to-comment'
+
+.image.js-replaced-image{ data: diff_view_data }
+ .two-up.view
+ .wrap
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(old_blob.size)
+ |
+ %strong W:
+ %span.meta-width
+ |
+ %strong H:
+ %span.meta-height
+ .wrap
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(blob.size)
+ |
+ %strong W:
+ %span.meta-width
+ |
+ %strong H:
+ %span.meta-height
+
+ .swipe.view.hide
+ .swipe-frame
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ .swipe-wrap
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ %span.swipe-bar
+ %span.top-handle
+ %span.bottom-handle
+
+ .onion-skin.view.hide
+ .onion-skin-frame
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ .controls
+ .transparent
+ .drag-track
+ .dragger{ :style => "left: 0px;" }
+ .opaque
+
+.view-modes.hide
+ %ul.view-modes-menu
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml
new file mode 100644
index 00000000000..6b0c6bbe48f
--- /dev/null
+++ b/app/views/projects/diffs/_single_image_diff.html.haml
@@ -0,0 +1,16 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- click_to_comment = local_assigns.fetch(:click_to_comment, true)
+- diff_view_data = local_assigns.fetch(:diff_view_data, '')
+- class_name = ''
+
+- if click_to_comment
+ - class_name = 'js-add-image-diff-note-button click-to-comment'
+
+.image.js-single-image{ data: diff_view_data }
+ .wrap
+ - single_class_name = diff_file.deleted_file? ? 'deleted' : 'added'
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.file_path }
+ %p.image-info= number_to_human_size(blob.size)
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index ad2d355ab4a..2de2cf9e38c 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -21,9 +21,9 @@
%ul
- diff_files.each do |diff_file|
%li
- %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
+ %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
= icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
- %span.diff-file-changes-path= diff_file.new_path
+ %span.diff-file-changes-path.append-right-5= diff_file.new_path
.pull-right
%span.cgreen<
+#{diff_file.added_lines}
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index aa004a739d7..f190073c2fc 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -1,69 +1,13 @@
- diff_file = viewer.diff_file
-- blob = diff_file.blob
-- old_blob = diff_file.old_blob
-- blob_raw_path = diff_file_blob_raw_path(diff_file)
-- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- image_point = Gitlab::Diff::ImagePoint.new(nil, nil, nil, nil)
+- discussions = @grouped_diff_discussions[diff_file.new_path] if @grouped_diff_discussions
+
+- locals = { diff_file: diff_file, position: diff_file.position(image_point, position_type: :image).to_json, click_to_comment: true, diff_view_data: diff_view_data }
- if diff_file.new_file? || diff_file.deleted_file?
- .image
- %span.wrap
- .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
- = image_tag(blob_raw_path, alt: diff_file.file_path)
- %p.image-info= number_to_human_size(blob.size)
+ = render partial: "projects/diffs/single_image_diff", locals: locals
- else
- .image
- .two-up.view
- %span.wrap
- .frame.deleted
- %a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(old_blob.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
- %span.wrap
- .frame.added
- %a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
- = image_tag(blob_raw_path, alt: diff_file.new_path)
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(blob.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
-
- .swipe.view.hide
- .swipe-frame
- .frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
- .swipe-wrap
- .frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path)
- %span.swipe-bar
- %span.top-handle
- %span.bottom-handle
-
- .onion-skin.view.hide
- .onion-skin-frame
- .frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
- .frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path)
- .controls
- .transparent
- .drag-track
- .dragger{ :style => "left: 0px;" }
- .opaque
-
+ = render partial: "projects/diffs/replaced_image_diff", locals: locals
- .view-modes.hide
- %ul.view-modes-menu
- %li.two-up{ data: { mode: 'two-up' } } 2-up
- %li.swipe{ data: { mode: 'swipe' } } Swipe
- %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
+.note-container
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0a3045604f4..8ae4fd94146 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -3,8 +3,6 @@
- @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test?
-= render "projects/settings/head"
-
.project-edit-container
%section.settings.general-settings
.settings-header
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 5e980314307..3f3ce10419f 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -3,7 +3,6 @@
= render partial: 'flash_messages', locals: { project: @project }
-= render "projects/head"
= render "home_panel"
.row-content-block.second-block.center
@@ -27,6 +26,8 @@
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
.prepend-top-20
.empty_wrapper
%h3.page-title-empty
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
index 3871165763c..d6ff3f729b4 100644
--- a/app/views/projects/environments/edit.html.haml
+++ b/app/views/projects/environments/edit.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Edit", @environment.name, "Environments"
-= render "projects/pipelines/head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index f7e3733ba0b..1bcc955ddc8 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Environments"
-= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index acc80b49dd0..2e85f608823 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- page_title "Environments"
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index e9e1ad9ef30..e0aedcac5e1 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -4,7 +4,6 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'monitoring'
-= render "projects/pipelines/head"
.prometheus-container{ class: container_class }
.top-area
@@ -16,6 +15,8 @@
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
+ "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'),
+ "empty-loading-svg-path": image_path('illustrations/monitoring/loading'),
+ "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
-
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index 88f43a1e7e4..62b08e85e22 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title "Environments"
- page_title 'New Environment'
-= render "projects/pipelines/head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index c35d1b5aaee..d7859c9fbeb 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs "Environments", project_environments_path(@project)
- breadcrumb_title @environment.name
- page_title "Environments"
-= render "projects/pipelines/head"
%div{ class: container_class }
.row.top-area.adjust
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 464135b5ac7..a073a164f11 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Terminal for environment", @environment.name
-= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 021575160ea..a3467eb6f05 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,5 +1,4 @@
- page_title "Find File", @ref
-= render "projects/commits/head"
.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, @options.merge(format: :json)))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) }
.nav-block
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 0f36e1a7353..e9613534dde 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -9,46 +9,36 @@
%br
Forking a repository allows you to make changes without affecting the original project.
.col-lg-9
- .fork-namespaces
- - if @namespaces.present?
- %label.label-light
- %span
- Click to fork the project to a user or group
- - @namespaces.in_groups_of(6, false) do |group|
- .row
- - group.each do |namespace|
- - avatar = namespace_icon(namespace, 100)
- - if fork = namespace.find_fork_of(@project)
- .fork-thumbnail.forked
- = link_to project_path(fork) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- .no-avatar
- = icon 'question'
- - else
- = image_tag avatar
- .caption
- = namespace.human_name
- - else
- .fork-thumbnail
- = link_to project_forks_path(@project, namespace_key: namespace.id), method: "POST" do
- - if /no_((\w*)_)*avatar/.match(avatar)
- .no-avatar
- = icon 'question'
- - else
- = image_tag avatar
- .caption
- = namespace.human_name
- - else
- %label.label-light
- %span
- No available namespaces to fork the project.
- %br
- %small
- You must have permission to create a project in a namespace before forking.
+ - if @namespaces.present?
+ .fork-thumbnail-container.js-fork-content
+ %h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
+ Click to fork the project
+ - @namespaces.each do |namespace|
+ - avatar = namespace_icon(namespace, 100)
+ - can_create_project = current_user.can?(:create_projects, namespace)
+ - forked_project = namespace.find_fork_of(@project)
+ - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
+ = link_to fork_path,
+ method: "POST",
+ class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
+ title: (_('You have reached your project limit') unless can_create_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
+ - else
+ %strong
+ No available namespaces to fork the project.
+ %p.prepend-top-default
+ You must have permission to create a project in a namespace before forking.
- .save-project-loader.hide
- .center
- %h2
- %i.fa.fa-spinner.fa-spin
- Forking repository
- %p Please wait a moment, this page will automatically refresh when ready.
+ .save-project-loader.hide.js-fork-content
+ %h2.text-center
+ = icon('spinner spin')
+ Forking repository
+ %p.text-center
+ Please wait a moment, this page will automatically refresh when ready.
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index f0ef647ddb3..ffb9238a65a 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -4,7 +4,6 @@
= webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs')
= webpack_bundle_tag('graphs_charts')
-= render "projects/commits/head"
.repo-charts{ class: container_class }
%h4.sub-header
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 08b38428b50..70156c03e3c 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -5,8 +5,6 @@
= webpack_bundle_tag('graphs')
= webpack_bundle_tag('graphs_show')
-= render 'projects/commits/head'
-
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block
.tree-ref-holder
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index ab5a7b117d7..1cf4105bd27 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -1,5 +1,3 @@
-= render 'projects/settings/head'
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index c8c17d2d828..b1219f019d7 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,5 +1,4 @@
- page_title 'Integrations'
-= render 'projects/settings/head'
.row.prepend-top-default
.col-lg-3
@@ -19,4 +18,3 @@
%hr
= render partial: 'projects/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs, project: @project }
-
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
deleted file mode 100644
index e9f21594a71..00000000000
--- a/app/views/projects/issues/_head.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to project_issues_path(@project), title: 'Issues' do
- %span
- List
-
- = nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: 'Board' do
- %span
- Board
-
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to project_merge_requests_path(@project), title: 'Merge Requests' do
- %span
- Merge Requests
-
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: 'Labels' do
- %span
- Labels
-
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: 'Milestones' do
- %span
- Milestones
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 7dc35be57a6..64c648f201b 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -24,7 +24,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title) do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 6fb5aa45166..6f7713124ac 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,7 +1,9 @@
+- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
+
%ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
- = render 'shared/empty_states/issues'
+ = render empty_state_path
- if @issues.present?
= paginate @issues, theme: "gitlab", total_pages: @total_pages
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 6a567487514..5f97d31f610 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,13 +2,13 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- - has_any_ci = @merge_requests.any?(&:head_pipeline)
+ - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline)
- - elsif has_any_ci
+ - elsif has_any_head_pipeline
= icon('blank fw')
%span.merge-request-id
= merge_request.to_reference
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
deleted file mode 100644
index 1b7d878c38c..00000000000
--- a/app/views/projects/issues/edit.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
-
-%h3.page-title
- Edit Issue ##{@issue.iid}
-%hr
-
-= render "form"
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index e72c94695bc..bfaf024428d 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -3,8 +3,6 @@
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
-= content_for :sub_nav do
- = render "projects/issues/head"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index fbaf88356bf..b9fec8af4d7 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -27,7 +27,9 @@
.issuable-meta
- if @issue.confidential
- = icon('eye-slash', class: 'is-confidential')
+ = icon('eye-slash', class: 'issuable-warning-icon')
+ - if @issue.discussion_locked?
+ = icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 8604c7d3ea4..9963cc93633 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Jobs"
-= render "projects/pipelines/head"
%div{ class: container_class }
.top-area
@@ -9,7 +8,7 @@
.nav-controls
- if can?(current_user, :update_build, @project)
- - if @all_builds.running_or_pending.any?
+ - if @all_builds.running_or_pending.limit(1).any?
= link_to 'Cancel running', cancel_all_project_jobs_path(@project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 975c08c06e6..ce0e3872240 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs "Jobs", project_jobs_path(@project)
- breadcrumb_title "##{@build.id}"
- page_title "#{@build.name} (##{@build.id})", "Jobs"
-= render "projects/pipelines/head"
%div{ class: container_class }
.build-page.js-build-page
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 84b0b65d1c0..b8ee4305142 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Edit", @label.name, "Labels"
-= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 10d07ce8e45..80e4dce1a80 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,8 +3,6 @@
- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
-= render "shared/mr_head"
-
- if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class }
.top-area.adjust
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 562b6fb8d8c..02f59f30a39 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title "Labels"
- page_title "New Label"
-= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml
deleted file mode 100644
index 1e505222887..00000000000
--- a/app/views/projects/merge_requests/_head.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :merge_requests) do
- = link_to project_merge_requests_path(@project), title: 'Merge Requests' do
- %span
- List
-
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: 'Labels' do
- %span
- Labels
-
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: 'Milestones' do
- %span
- Milestones
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 0a1ebcb8124..2b5e8711b0a 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title) do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index f3c44c94a5c..cb723fe6a18 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -15,6 +15,8 @@
= icon('angle-double-left')
.issuable-meta
+ - if @merge_request.discussion_locked?
+ = icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
@@ -29,7 +31,7 @@
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
- %li{ class: merge_request_button_visibility(@merge_request, true) }
+ %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 2c53891a92d..6b8dcb3e60b 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -4,9 +4,6 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
-- unless @project.issues_enabled?
- = content_for :sub_nav do
- = render "projects/merge_requests/head"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index c2d16f7e731..d88e3d794d3 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -62,7 +62,10 @@
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
+ %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
+ = render 'shared/icons/icon_status_success_solid.svg'
+ %template{ 'v-else' => '' }
+ = render 'shared/icons/icon_resolve_discussion.svg'
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
@@ -80,7 +83,7 @@
#pipelines.pipelines.tab-pane
- if @pipelines.any?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- #diffs.diffs.tab-pane
+ #diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } }
-# This tab is always loaded via AJAX
.mr-loading-status
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 1e66c6079e3..af3f25c6a30 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "Edit", @milestone.title, "Milestones"
-= render "shared/mr_head"
%div{ class: container_class }
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index f3abecdd302..fcbf7cb802b 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,8 +1,6 @@
- @no_container = true
- page_title 'Milestones'
-= render "shared/mr_head"
-
%div{ class: container_class }
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 84ffbc0a926..c301f517013 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title "Milestones"
- page_title "New Milestone"
-= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 1f5f18801ad..a5153df1159 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,7 +3,6 @@
- breadcrumb_title @milestone.title
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
-= render "shared/mr_head"
%div{ class: container_class }
.detail-page-header.milestone-page-header
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index e29cb277389..8a19497c55b 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -2,7 +2,6 @@
- page_title "Graph", @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('network')
-= render "projects/commits/head"
= render "head"
%div{ class: container_class }
.project-network
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 819392b8f0c..cc41b908946 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,7 +16,9 @@
New project
- if import_sources_enabled?
%p
- Create or Import your project from popular Git services
+ A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ %p
+ All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
.col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
.create-project-options
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index de76832331a..4961835f12a 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -1,7 +1,8 @@
+- access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
- %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") }
+ %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon
-- if access = note_max_access_for_user(note)
+- if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access)
- if note.resolvable?
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 098b0ef56ef..04e647c0dc6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,5 +1,4 @@
- page_title 'Pages'
-= render "projects/settings/head"
%h3.page_title
Pages
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 2b081786b6a..4fbdd1dd5e4 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -7,8 +7,6 @@
- @no_container = true
- page_title _("Pipeline Schedules")
-= render "projects/pipelines/head"
-
%div{ class: container_class }
#pipeline-schedules-callout{ data: { docs_url: help_page_path('user/project/pipelines/schedules') } }
.top-area
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
deleted file mode 100644
index ee2f236cec4..00000000000
--- a/app/views/projects/pipelines/_head.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(path: ['pipelines#index', 'pipelines#show']) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
-
- - if project_nav_tab? :builds
- = nav_link(controller: [:jobs, :artifacts]) do
- = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
- %span
- Jobs
-
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipeline_schedules) do
- = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do
- %span
- Schedules
-
- - if project_nav_tab? :environments
- = nav_link(controller: :environments) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
-
- - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
- = nav_link(path: 'pipelines#charts') do
- = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
- %span
- Charts
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 487ac87186d..ba55bc23add 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
-= render 'head'
%div{ class: container_class }
.sub-header-block
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 9fc15d20f34..a10a7c23924 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -1,14 +1,14 @@
- @no_container = true
- page_title "Pipelines"
-= render "projects/pipelines/head"
%div{ 'class' => container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
-
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
+ "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
+ "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
@@ -17,7 +17,7 @@
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
- "has-ci" => @project.has_ci?,
+ "has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 7cc9fe79afd..2174154b207 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs "Pipelines", project_pipelines_path(@project)
- breadcrumb_title "##{@pipeline.id}"
- page_title "Pipeline"
-= render "projects/pipelines/head"
.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
- if @commit
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 324cd423ede..77211099830 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -3,11 +3,15 @@
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
.form-group
- %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery.
%h5 Auto DevOps (Beta)
%p
- Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration.
+ Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
+ This will happen starting with the next event (e.g.: push) that occurs to the project.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
+ - message = auto_devops_warning_message(@project)
+ - if message
+ %p.settings-message.text-center
+ = message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
@@ -15,26 +19,25 @@
%strong Enable Auto DevOps
%br
%span.descr
- The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
- in the project.
+ The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
- A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
+ An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
+
.radio
- = form.label :enabled do
- = form.radio_button :enabled, nil
- %strong
- Instance default (status: #{current_application_settings.auto_devops_enabled?})
+ = form.label :enabled_nil do
+ = form.radio_button :enabled, ''
+ %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
- Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
+ Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%br
%p
- Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
+ You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml
deleted file mode 100644
index a0535edafc3..00000000000
--- a/app/views/projects/registry/repositories/_image.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.container-image.js-toggle-container
- .container-image-head
- = link_to "#", class: "js-toggle-button" do
- = icon('chevron-down', 'aria-hidden': 'true')
- = escape_once(image.path)
-
- = clipboard_button(clipboard_text: "docker pull #{image.location}")
-
- - if can?(current_user, :update_container_image, @project)
- .controls.hidden-xs.pull-right
- = link_to project_container_registry_path(@project, image),
- class: 'btn btn-remove has-tooltip',
- title: 'Remove repository',
- data: { confirm: 'Are you sure?' },
- method: :delete do
- = icon('trash cred', 'aria-hidden': 'true')
-
- .container-image-tags.js-toggle-content.hide
- - if image.has_tags?
- .table-holder
- %table.table.tags
- %thead
- %tr
- %th Tag
- %th Tag ID
- %th Size
- %th Created
- - if can?(current_user, :update_container_image, @project)
- %th
- = render partial: 'tag', collection: image.tags
- - else
- .nothing-here-block No tags in Container Registry for this container image.
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 5661af01302..36ea5e013e4 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,60 +1,49 @@
- page_title "Container Registry"
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
+%section
+ .settings-header
+ %h4
= page_title
%p
- With the Docker Container Registry integrated into GitLab, every project
- can have its own space to store its Docker images.
+ = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
- Learn more about
- = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
+ = s_('ContainerRegistry|Learn more about')
+ = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
+ .row.registry-placeholder.prepend-bottom-10
+ .col-lg-12
+ #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
- .col-lg-9
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- How to use the Container Registry
- .panel-body
- %p
- First log in to GitLab&rsquo;s Container Registry using your GitLab username
- and password. If you have
- = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
- you need to use a
- = succeed ':' do
- = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
- %pre
- docker login #{Gitlab.config.registry.host_port}
- %br
- %p
- Once you log in, you&rsquo;re free to create and upload a container image
- using the common
- %code build
- and
- %code push
- commands:
- %pre
- :plain
- docker build -t #{escape_once(@project.container_registry_url)} .
- docker push #{escape_once(@project.container_registry_url)}
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('registry_list')
- %hr
- %h5.prepend-top-default
- Use different image names
- %p.light
- GitLab supports up to 3 levels of image names. The following
- examples of images are valid for your project:
- %pre
- :plain
- #{escape_once(@project.container_registry_url)}:tag
- #{escape_once(@project.container_registry_url)}/optional-image-name:tag
- #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
-
- - if @images.blank?
- %p.settings-message.text-center.append-bottom-default
- No container images stored for this project. Add one by following the
- instructions above.
- - else
- = render partial: 'image', collection: @images
+ .row.prepend-top-10
+ .col-lg-12
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ = s_('ContainerRegistry|How to use the Container Registry')
+ .panel-body
+ %p
+ - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
+ - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
+ = s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
+ %pre
+ docker login #{Gitlab.config.registry.host_port}
+ %br
+ %p
+ = s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
+ %pre
+ :plain
+ docker build -t #{escape_once(@project.container_registry_url)} .
+ docker push #{escape_once(@project.container_registry_url)}
+ %hr
+ %h5.prepend-top-default
+ = s_('ContainerRegistry|Use different image names')
+ %p.light
+ = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
+ %pre
+ :plain
+ #{escape_once(@project.container_registry_url)}:tag
+ #{escape_once(@project.container_registry_url)}/optional-image-name:tag
+ #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index c786298e341..4d962f9433f 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs "Tags", project_tags_path(@project)
- breadcrumb_title @tag.name
- page_title "Edit", @tag.name, "Tags"
-= render "projects/commits/head"
%div{ class: container_class }
.sub-header-block.no-bottom-space
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index ac8e15a48b2..e660fce652f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -11,7 +11,7 @@
.col-sm-10
.checkbox
= f.check_box :access_level, {}, 'ref_protected', 'not_protected'
- %span.light This runner will only run on pipelines trigged on protected branches
+ %span.light This runner will only run on pipelines triggered on protected branches
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
@@ -39,6 +39,6 @@
Tags
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .help-block You can setup jobs to only use Runners with specific tags
+ .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index b842fd57cf3..c0b1c62e8ef 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -23,7 +23,7 @@
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
- = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
+ = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 3e2a24a4c32..25770df1c90 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -2,5 +2,4 @@
- page_title @service.title, "Services"
- add_to_breadcrumbs("Settings", edit_project_path(@project))
-= render "projects/settings/head"
= render 'form'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
deleted file mode 100644
index 7d24c6a9122..00000000000
--- a/app/views/projects/settings/_head.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: container_class }
- - can_edit = can?(current_user, :admin_project, @project)
- - if can_edit
- = nav_link(controller: :projects) do
- = link_to edit_project_path(@project), title: 'General' do
- %span
- General
- - if can_edit
- = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
- = link_to project_settings_integrations_path(@project), title: 'Integrations' do
- %span
- Integrations
- = nav_link(controller: :repository) do
- = link_to project_settings_repository_path(@project), title: 'Repository' do
- %span
- Repository
- - if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :ci_cd) do
- = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do
- %span
- Pipelines
- - if @project.pages_available?
- = nav_link(controller: :pages) do
- = link_to project_pages_path(@project), title: 'Pages' do
- %span
- Pages
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index d4f71d023c6..62455d0d40d 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -2,8 +2,6 @@
- page_title "CI / CD Settings"
- page_title "CI / CD"
-= render "projects/settings/head"
-
- expanded = Rails.env.test?
%section.settings#js-general-pipeline-settings
@@ -13,7 +11,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
- Update your CI/CD configuration, like job timeout.
+ Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index d5792e95f5a..82516cb4bcf 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -10,7 +10,7 @@
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
- = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-small'
+ = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-sm'
= link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
%span.sr-only Remove
= icon('trash')
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 933daa7f549..2f1a548e119 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,6 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title "Integrations Settings"
- page_title 'Integrations'
-= render "projects/settings/head"
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index 1e7695ac397..ea2cd36b212 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,6 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
- page_title "Members"
-= render "projects/settings/head"
= render "projects/project_members/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 6d4af72b8ea..517d51993d2 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,8 +2,6 @@
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
-= render "projects/settings/head"
-
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('deploy_keys')
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index d8f5114f4b5..705a4607ad2 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -7,7 +7,6 @@
= render partial: 'flash_messages', locals: { project: @project }
-= render "projects/head"
= render "projects/last_push"
= render "home_panel"
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index fda068f08c2..7062c5b765e 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
-- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 468ab922542..1927216e191 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,12 +2,11 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row
.row-main-content.str-truncated
- = link_to project_tag_path(@project, tag.name), class: 'item-title ref-name' do
- = icon('tag')
- = tag.name
+ = icon('tag')
+ = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag)
- %span.label.label-success
+ %span.label.label-success.prepend-left-4
protected
- if tag.message.present?
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index a6fe02fcae0..27d58d4c0e8 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -2,7 +2,6 @@
- @sort ||= sort_value_recently_updated
- page_title "Tags"
- add_to_breadcrumbs("Repository", project_tree_path(@project))
-= render "projects/commits/head"
.flex-list{ class: container_class }
.top-area.adjust
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 5d6eb4f4026..43aa2b27af6 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs "Tags", project_tags_path(@project)
- breadcrumb_title @tag.name
- page_title @tag.name, "Tags"
-= render "projects/commits/head"
%div{ class: container_class }
.top-area.multi-line
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
index 820b947804e..6ea78851b8d 100644
--- a/app/views/projects/tree/_old_tree_content.html.haml
+++ b/app/views/projects/tree/_old_tree_content.html.haml
@@ -6,7 +6,7 @@
%th= s_('ProjectFileTree|Name')
%th.hidden-xs
.pull-left= _('Last commit')
- %th.text-right= _('Last Update')
+ %th.text-right= _('Last update')
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
index 13705ca303b..3a43dde8052 100644
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ b/app/views/projects/tree/_old_tree_header.html.haml
@@ -20,15 +20,12 @@
- if can_edit_tree?
%li
= link_to project_new_blob_path(@project, @id) do
- = icon('pencil fw')
#{ _('New file') }
%li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- = icon('file fw')
#{ _('Upload file') }
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- = icon('folder fw')
#{ _('New directory') }
- elsif can?(current_user, :fork_project, @project)
%li
@@ -38,7 +35,6 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('pencil fw')
#{ _('New file') }
%li
- continue_params = { to: request.fullpath,
@@ -47,7 +43,6 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('file fw')
#{ _('Upload file') }
%li
- continue_params = { to: request.fullpath,
@@ -56,15 +51,12 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('folder fw')
#{ _('New directory') }
%li.divider
%li
= link_to new_project_branch_path(@project) do
- = icon('code-fork fw')
#{ _('New branch') }
%li
= link_to new_project_tag_path(@project) do
- = icon('tags fw')
#{ _('New tag') }
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 853e2a6e7ec..6cddc38d11a 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,10 +1,6 @@
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
- - if show_new_repo?
- .tree-ref-target-holder.js-tree-ref-target-holder
- = icon('long-arrow-right', title: 'to target branch')
- = render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo?
= render 'projects/tree/old_tree_header'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index d84a1fd7ee1..0cc6674842a 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -11,10 +11,8 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
-= render "projects/commits/head"
-
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- - if show_auto_devops_callout?(@project)
+ - if show_auto_devops_callout?(@project) && !show_new_repo?
= render 'shared/auto_devops_callout'
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index e5a1fccf9ba..4e265bf733a 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,5 @@
-- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}"
+- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
+- commit_message = commit_message % { page_title: @page.title }
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
@@ -12,13 +13,13 @@
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
- = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control"
+ = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control'
.form-group
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...")
= render 'shared/notes/hints'
.clearfix
@@ -26,12 +27,11 @@
.help-block
= succeed '.' do
- To link to a (new) page, simply type
- %code [Link Title](page-slug)
+ = (s_("WikiMarkdownTip|To link to a (new) page, simply type %{link_example}") % { link_example: '<code>[Link Title](page-slug)</code>' }).html_safe
= succeed '.' do
- More examples are in the
- = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown")
+ - markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown')
+ = (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe
.form-group
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
@@ -39,10 +39,10 @@
.form-actions
- if @page && @page.persisted?
- = f.submit 'Save changes', class: "btn-save btn"
+ = f.submit _("Save changes"), class: 'btn-save btn'
.pull-right
- = link_to "Cancel", project_wiki_path(@project, @page), class: "btn btn-cancel btn-grouped"
+ = link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
- = f.submit 'Create page', class: "btn-create btn"
+ = f.submit s_("Wiki|Create page"), class: 'btn-create btn'
.pull-right
- = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel"
+ = link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 3bbd8042c3a..cadda0a33c2 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,9 @@
- if (@page && @page.persisted?)
- if can?(current_user, :create_wiki, @project)
= link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New page
+ = s_("Wiki|New page")
= link_to project_wiki_history_path(@project, @page), class: "btn" do
- Page history
+ = s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @project) && @page.latest?
= link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do
- Edit
+ = _("Edit")
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 13dd8461433..06a3cac12d5 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -3,16 +3,15 @@
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
- %h3.page-title New Wiki Page
+ %h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
.modal-body
%form.new-wiki-page
.form-group
= label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
+ %span= s_("WikiPage|Page slug")
+ = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
%span.new-wiki-page-slug-tip
= icon('lightbulb-o')
- Tip: You can specify the full path for the new file.
- We will automatically create any missing directories.
+ = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-actions
- = button_tag 'Create page', class: 'build-new-wiki btn btn-create'
+ = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-create"
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index 7c2f562d422..0a1ccbc5f1c 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
.pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index f7283ae4739..5b781294d68 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -8,7 +8,7 @@
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
= succeed '&nbsp;' do
= icon('cloud-download')
- Clone repository
+ = _("Clone repository")
.blocks-container
.block.block-first
@@ -17,6 +17,6 @@
.block
= link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
- More Pages
+ = s_("Wiki|More Pages")
= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 8fd60216536..0d77e5bd16d 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,11 +1,10 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
-- page_title "Edit", @page.title.capitalize, "Wiki"
+- page_title _("Edit"), @page.title.capitalize, _("Wiki")
- if @conflict
.alert.alert-danger
- Someone edited the page the same time you did. Please check out
- = link_to "the page", project_wiki_path(@project, @page), target: "_blank"
- and make sure your changes will not unintentionally remove theirs.
+ - page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
+ = (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -20,20 +19,20 @@
%span.light
&middot;
- if @page.persisted?
- Edit Page
+ = s_("Wiki|Edit Page")
- else
- Create Page
+ = s_("Wiki|Create Page")
.nav-controls
- if can?(current_user, :create_wiki, @project)
= link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New page
+ = s_("Wiki|New page")
- if @page.persisted?
= link_to project_wiki_history_path(@project, @page), class: "btn" do
- Page history
+ = s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- = link_to project_wiki_path(@project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do
- Delete
+ = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do
+ = _("Delete")
= render 'form'
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index 7dfa405d063..911e1339541 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,6 +1,6 @@
-- page_title "Wiki"
+- page_title _("Wiki")
-%h3.page-title Empty page
+%h3.page-title= _("Wiki|Empty page")
%hr
.error_message
- You are not allowed to create wiki pages
+ = s_("WikiEmptyPageError|You are not allowed to create wiki pages")
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index e740fb93ea4..10dbbc0e42c 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,36 +1,34 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
-- page_title "Git Access", "Wiki"
+- page_title s_("WikiClone|Git Access"), _("Wiki")
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
.git-access-header
- Clone repository
+ = _("Clone repository")
%strong= @project_wiki.full_path
= render "shared/clone_panel", project: @project_wiki
.wiki-git-access
- %h3 Install Gollum
+ %h3= s_("WikiClone|Install Gollum")
%pre.dark
:preserve
gem install gollum
%p
- It is recommended to install
- %code github-markdown
- so that GFM features render locally:
+ = (s_("WikiClone|It is recommended to install %{markdown} so that GFM features render locally:") % { markdown: "<code>github-markdown</code>" }).html_safe
%pre.dark
:preserve
gem install github-markdown
- %h3 Clone your wiki
+ %h3= s_("WikiClone|Clone your wiki")
%pre.dark
:preserve
git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')}
cd #{h @project_wiki.path}
- %h3 Start Gollum and edit locally
+ %h3= s_("WikiClone|Start Gollum and edit locally")
%pre.dark
:preserve
gollum
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 306feeff259..9ee09262324 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,4 +1,4 @@
-- page_title "History", @page.title.capitalize, "Wiki"
+- page_title _("History"), @page.title.capitalize, _("Wiki")
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -9,17 +9,17 @@
= link_to @page.title.capitalize, project_wiki_path(@project, @page)
%span.light
&middot;
- History
+ = _("History")
.table-holder
%table.table
%thead
%tr
- %th Page version
- %th Author
- %th Commit Message
- %th Last updated
- %th Format
+ %th= s_("Wiki|Page version")
+ %th= _("Author")
+ %th= _("Commit Message")
+ %th= _("Last updated")
+ %th= _("Format")
%tbody
- @page.versions.each_with_index do |version, index|
- commit = version
@@ -29,13 +29,13 @@
commit.id, index == 0) do
= truncate_sha(commit.id)
%td
- = commit.author.name
+ = commit.author_name
%td
= commit.message
%td
#{time_ago_with_tooltip(version.authored_date)}
%td
%strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ = version.format
= render 'sidebar'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index d533c611a38..aeef64fd7eb 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,19 +1,19 @@
- @no_container = true
- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
-- breadcrumb_title "Pages"
-- page_title "Pages", "Wiki"
+- breadcrumb_title s_("Wiki|Pages")
+- page_title s_("Wiki|Pages"), _("Wiki")
%div{ class: container_class }
.wiki-page-header
.nav-text
%h2.wiki-page-title
- Wiki Pages
+ = s_("Wiki|Wiki Pages")
.nav-controls
= link_to project_wikis_git_access_path(@project), class: 'btn' do
= icon('cloud-download')
- Clone repository
+ = _("Clone repository")
%ul.wiki-pages-list.content-list
= render @wiki_entries, context: 'pages'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index b066a812ec8..de15fc99eda 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,8 +1,8 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- breadcrumb_title @page.title.capitalize
- wiki_breadcrumb_dropdown_links(@page.slug)
-- page_title @page.title.capitalize, "Wiki"
-- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
+- page_title @page.title.capitalize, _("Wiki")
+- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -11,9 +11,7 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- Last edited by
- %strong
- #{@page.commit.author.name}
+ = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls
@@ -21,8 +19,10 @@
- if @page.historical?
.warning_message
- This is an old version of this page.
- You can view the #{link_to "most recent version", project_wiki_path(@project, @page)} or browse the #{link_to "history", project_wiki_history_path(@project, @page)}.
+ = s_("WikiHistoricalPage|This is an old version of this page.")
+ - most_recent_link = link_to s_("WikiHistoricalPage|most recent version"), project_wiki_path(@project, @page)
+ - history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page)
+ = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 2f09c2fec87..7c633175a06 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -6,10 +6,10 @@
.svg-container
= custom_icon('icon_autodevops')
.user-callout-copy
- %h4= _('Auto DevOps (Beta)')
- %p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+ %h4= s_('AutoDevOps|Auto DevOps (Beta)')
+ %p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- #{s_('AutoDevOps|Learn more in the')}
- = link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
- = link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
+ = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
diff --git a/app/views/shared/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml
new file mode 100644
index 00000000000..b7bbc109238
--- /dev/null
+++ b/app/views/shared/_email_with_badge.html.haml
@@ -0,0 +1,8 @@
+- css_classes = %w(label label-verification-status)
+- css_classes << (verified ? 'verified': 'unverified')
+- text = verified ? 'Verified' : 'Unverified'
+
+.email-badge
+ .email-badge-email= email
+ %div{ class: css_classes }
+ = text
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 2e1bd5a088c..d0b9e891b82 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -22,11 +22,9 @@
- if @group.persisted?
.alert.alert-warning.prepend-top-10
- %ul
- %li Changing group path can have unintended side effects.
- %li Renaming group path will rename directory for all related projects
- %li It will change web url for access group and group projects.
- %li It will change the git path to repositories under this group.
+ Changing group path can have unintended side effects.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.form-group.group-name-holder
= f.label :name, class: 'control-label' do
diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml
deleted file mode 100644
index e7355ae2eea..00000000000
--- a/app/views/shared/_mr_head.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- if @project.issues_enabled?
- = render "projects/issues/head"
-- else
- = render "projects/merge_requests/head"
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
deleted file mode 100644
index 61646f150c1..00000000000
--- a/app/views/shared/_nav_scroll.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.fade-left
- = icon('angle-left')
-.fade-right
- = icon('angle-right')
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index e415ec64c38..b8b1f4ca42f 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -1,9 +1,9 @@
- type = impersonation ? "impersonation" : "personal access"
%h5.prepend-top-0
- Add a #{type} Token
+ Add a #{type} token
%p.profile-settings-content
- Pick a name for the application, and we'll give you a unique #{type} Token.
+ Pick a name for the application, and we'll give you a unique #{type} token.
= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index eb5ddb0dde4..2530db986e0 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,8 +1,8 @@
%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
- = icon('angle-double-left')
- = icon('angle-double-right')
+ = sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
+ = sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
%span.collapse-text Collapse sidebar
= button_tag class: 'close-nav-button', type: 'button' do
- = icon ('times')
+ = sprite_icon('close', size: 16)
%span.collapse-text Close sidebar
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 785a500e44e..7ff5e679f17 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,36 +1,16 @@
+- sorted_by = sort_options_hash[@sort]
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ = sorted_by
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-sort
%li
- = link_to page_filter_path(sort: sort_value_priority, label: true) do
- = sort_title_priority
- = link_to page_filter_path(sort: sort_value_label_priority, label: true) do
- = sort_title_label_priority
- = link_to page_filter_path(sort: sort_value_recently_created, label: true) do
- = sort_title_recently_created
- = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
- = sort_title_oldest_created
- = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do
- = sort_title_recently_updated
- = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
- = sort_title_oldest_updated
- = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
- = sort_title_milestone_soon
- = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
- = sort_title_milestone_later
- - if viewing_issues
- = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
- = sort_title_due_date_soon
- = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
- = sort_title_due_date_later
- = link_to page_filter_path(sort: sort_value_upvotes, label: true) do
- = sort_title_upvotes
- = link_to page_filter_path(sort: sort_value_downvotes, label: true) do
- = sort_title_downvotes
+ = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
+ = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
+ = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by)
+ = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues
+ = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by)
+ = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sorted_by)
diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml
deleted file mode 100644
index 9236868652f..00000000000
--- a/app/views/shared/_target_switcher.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- dropdown_toggle_text = @ref || @project.default_branch
-= form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
- = hidden_field_tag :destination, destination
- - if defined?(path)
- = hidden_field_tag :path, path
- - @options && @options.each do |key, value|
- = hidden_field_tag key, value, id: nil
- .dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" }
- %ul.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
- %li
- = dropdown_title _("Create a new branch")
- %li
- = dropdown_input _("Create a new branch")
- %li
- = dropdown_title _("Select existing branch"), options: {close: false}
- %li
- = dropdown_filter _("Search branches and tags")
- = dropdown_content
- = dropdown_loading
diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml
deleted file mode 100644
index 17ffcba69d8..00000000000
--- a/app/views/shared/_user_callout.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.user-callout{ data: { uid: 'user_callout_dismissed' } }
- .bordered-box.landing.content-block
- %button.btn.btn-default.close.js-close-callout{ type: 'button',
- 'aria-label' => 'Dismiss customize experience box' }
- = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
- .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/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 1a50b7d4b69..ee8ad8e3999 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -12,13 +12,11 @@
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
-= render "projects/issues/head"
+#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .hidden-xs.hidden-sm
+ = render 'shared/issuable/search_bar', type: :boards
-.hidden-xs.hidden-sm
- = render 'shared/issuable/search_bar', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-list
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
%board{ "v-cloak" => true,
@@ -31,10 +29,11 @@
":board-id" => "boardId",
":key" => "_uid" }
= render "shared/boards/components/sidebar"
- %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
- "new-issue-path" => new_project_issue_path(@project),
- "milestone-path" => milestones_filter_dropdown_path,
- "label-path" => labels_filter_path,
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":project-id" => @project.try(:id) }
+ - if @project
+ %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
+ "milestone-path" => milestones_filter_dropdown_path,
+ "label-path" => labels_filter_path,
+ "empty-state-svg" => image_path('illustrations/issues.svg'),
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":project-id" => @project.id }
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index ce0aa72ab00..c687e66fd43 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -8,7 +8,7 @@
"aria-hidden": "true" }
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
- ":title" => '(list.label ? list.label.description : "")' }
+ ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
{{ list.title }}
%span.has-tooltip{ "v-if": "list.type === \"label\"",
@@ -27,7 +27,7 @@
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
- %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
+ %button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => "New issue",
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 3baa956b910..639f28cc210 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -3,22 +3,22 @@
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
- = number_with_delimiter(all_builds.count(:id))
+ = limited_counter_with_delimiter(all_builds)
%li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
- = number_with_delimiter(all_builds.pending.count(:id))
+ = limited_counter_with_delimiter(all_builds.pending)
%li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
- = number_with_delimiter(all_builds.running.count(:id))
+ = limited_counter_with_delimiter(all_builds.running)
%li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
- = number_with_delimiter(all_builds.finished.count(:id))
+ = limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 68737e8da66..de26fa8bbf3 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -5,7 +5,7 @@
.row.empty-state
.col-xs-12
.svg-content
- = render 'shared/empty_states/icons/issues.svg'
+ = image_tag 'illustrations/issues.svg'
.col-xs-12.text-center
.text-content
- if has_button && current_user
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index bfda522f2f6..a65634dce53 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,7 +1,7 @@
.row.empty-state.labels
.col-xs-12
.svg-content
- = render 'shared/empty_states/icons/labels.svg'
+ = image_tag 'illustrations/labels.svg'
.col-xs-12.text-center
.text-content
%h4 Labels can be applied to issues and merge requests to categorize them.
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index ff5741b6d61..67f906903e9 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -5,7 +5,7 @@
.row.empty-state.merge-requests
.col-xs-12
.svg-content
- = render 'shared/empty_states/icons/merge_requests.svg'
+ = image_tag 'illustrations/merge_requests.svg'
.col-xs-12.text-center
.text-content
- if has_button
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
index bc268301a97..555cb4f4af9 100644
--- a/app/views/shared/empty_states/_priority_labels.html.haml
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -1,3 +1,4 @@
.text-center
- = render 'shared/empty_states/icons/priority_labels.svg'
+ .svg-content
+ = image_tag 'illustrations/priority_labels.svg'
%p Star labels to start sorting by priority
diff --git a/app/views/shared/empty_states/icons/_issues.svg b/app/views/shared/empty_states/icons/_issues.svg
deleted file mode 100644
index 2e92bf19579..00000000000
--- a/app/views/shared/empty_states/icons/_issues.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg>
diff --git a/app/views/shared/empty_states/icons/_labels.svg b/app/views/shared/empty_states/icons/_labels.svg
deleted file mode 100644
index dc041a4a78b..00000000000
--- a/app/views/shared/empty_states/icons/_labels.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zM35 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM374.7 133.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zM318 226.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg>
diff --git a/app/views/shared/empty_states/icons/_merge_requests.svg b/app/views/shared/empty_states/icons/_merge_requests.svg
deleted file mode 100644
index e77f6319a95..00000000000
--- a/app/views/shared/empty_states/icons/_merge_requests.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg
deleted file mode 100644
index 7c672538097..00000000000
--- a/app/views/shared/empty_states/icons/_pipelines_empty.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g transform="translate(0 102)"><g fill="#e5e5e5"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g fill="#31af64" transform="translate(0 4)"><path fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="m64.82 76h33.18c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855c1.725 1.835 4.631 1.833 6.354 0l9.263-9.855"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26" transform="matrix(.70711-.70711.70711.70711 84.34 49.5)"/></g></svg>
diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg
deleted file mode 100644
index 7dbabf7e4ef..00000000000
--- a/app/views/shared/empty_states/icons/_pipelines_failed.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/icons/_priority_labels.svg b/app/views/shared/empty_states/icons/_priority_labels.svg
deleted file mode 100644
index 7282c2b215e..00000000000
--- a/app/views/shared/empty_states/icons/_priority_labels.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zM10.4 9.2C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg>
diff --git a/app/views/shared/empty_states/icons/_todos_all_done.svg b/app/views/shared/empty_states/icons/_todos_all_done.svg
deleted file mode 100644
index 94b5c2e0ea0..00000000000
--- a/app/views/shared/empty_states/icons/_todos_all_done.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
diff --git a/app/views/shared/empty_states/icons/_todos_empty.svg b/app/views/shared/empty_states/icons/_todos_empty.svg
deleted file mode 100644
index b1e661268fb..00000000000
--- a/app/views/shared/empty_states/icons/_todos_empty.svg
+++ /dev/null
@@ -1,110 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink">
- <defs>
- <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/>
- <mask id="l" width="180" height="220" x="0" y="0" fill="#fff">
- <use xlink:href="#a"/>
- </mask>
- <rect id="b" width="180" height="220" rx="6"/>
- <mask id="m" width="180" height="220" x="0" y="0" fill="#fff">
- <use xlink:href="#b"/>
- </mask>
- <rect id="c" width="28" height="28" rx="4"/>
- <mask id="n" width="28" height="28" x="0" y="0" fill="#fff">
- <use xlink:href="#c"/>
- </mask>
- <rect id="d" width="28" height="28" rx="4"/>
- <mask id="o" width="28" height="28" x="0" y="0" fill="#fff">
- <use xlink:href="#d"/>
- </mask>
- <circle id="e" cx="21.5" cy="21.5" r="21.5"/>
- <mask id="p" width="43" height="43" x="0" y="0" fill="#fff">
- <use xlink:href="#e"/>
- </mask>
- <circle id="f" cx="26.5" cy="26.5" r="26.5"/>
- <mask id="q" width="53" height="53" x="0" y="0" fill="#fff">
- <use xlink:href="#f"/>
- </mask>
- <circle id="g" cx="9.5" cy="4.5" r="4.5"/>
- <mask id="r" width="13" height="13" x="-2" y="-2">
- <path fill="#fff" d="M3-2h13v13H3z"/>
- <use xlink:href="#g"/>
- </mask>
- <circle id="h" cx="26.5" cy="26.5" r="26.5"/>
- <mask id="s" width="53" height="53" x="0" y="0" fill="#fff">
- <use xlink:href="#h"/>
- </mask>
- <circle id="i" cx="21.5" cy="21.5" r="21.5"/>
- <mask id="t" width="43" height="43" x="0" y="0" fill="#fff">
- <use xlink:href="#i"/>
- </mask>
- <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/>
- <mask id="u" width="52" height="44" x="0" y="0" fill="#fff">
- <use xlink:href="#j"/>
- </mask>
- <circle id="k" cx="18.5" cy="18.5" r="18.5"/>
- <mask id="v" width="37" height="37" x="0" y="0" fill="#fff">
- <use xlink:href="#k"/>
- </mask>
- </defs>
- <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)">
- <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/>
- <g transform="rotate(5 -707.333 618.042)">
- <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/>
- <g transform="translate(29 24)">
- <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
- <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
- <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/>
- <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/>
- </g>
- <g transform="translate(29 69)">
- <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
- <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
- <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/>
- <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/>
- </g>
- <g transform="translate(28 160)">
- <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/>
- <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/>
- <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/>
- </g>
- <g transform="translate(28 116)">
- <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/>
- <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/>
- <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/>
- </g>
- </g>
- <g transform="rotate(-15 601.917 -782.362)">
- <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/>
- <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1">
- <tspan x="12" y="27">@</tspan>
- </text>
- </g>
- <g transform="rotate(15 -686.59 1035.907)">
- <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/>
- <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/>
- <g transform="translate(17 14)">
- <use fill="#FC6D26" xlink:href="#g"/>
- <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/>
- </g>
- </g>
- <g transform="rotate(15 -85.125 65.185)">
- <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/>
- <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/>
- </g>
- <g transform="rotate(-15 716.492 78.873)">
- <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/>
- <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/>
- </g>
- <g transform="rotate(-15 129.114 -585.74)">
- <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/>
- <circle cx="16" cy="20" r="2" fill="#FC6D26"/>
- <circle cx="27" cy="20" r="2" fill="#FC6D26"/>
- <circle cx="38" cy="20" r="2" fill="#FC6D26"/>
- </g>
- <g transform="rotate(-15 1254.8 -458.986)">
- <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/>
- <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/>
- <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/>
- </g>
- </g>
-</svg>
diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg
index bd5d04e1cd7..bd5d04e1cd7 100755..100644
--- a/app/views/shared/icons/_icon_status_canceled.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
index 326ad04e017..326ad04e017 100755..100644
--- a/app/views/shared/icons/_icon_status_created.svg
+++ b/app/views/shared/icons/_icon_status_created.svg
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
index 64da5aa31fc..64da5aa31fc 100755..100644
--- a/app/views/shared/icons/_icon_status_failed.svg
+++ b/app/views/shared/icons/_icon_status_failed.svg
diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg
index c98839f51a9..c98839f51a9 100755..100644
--- a/app/views/shared/icons/_icon_status_manual.svg
+++ b/app/views/shared/icons/_icon_status_manual.svg
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
index 02d5da407e3..02d5da407e3 100755..100644
--- a/app/views/shared/icons/_icon_status_pending.svg
+++ b/app/views/shared/icons/_icon_status_pending.svg
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
index 532f4fee33c..532f4fee33c 100755..100644
--- a/app/views/shared/icons/_icon_status_running.svg
+++ b/app/views/shared/icons/_icon_status_running.svg
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index a9ba29c922c..a9ba29c922c 100755..100644
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
index eed5006bebe..eed5006bebe 100755..100644
--- a/app/views/shared/icons/_icon_status_success.svg
+++ b/app/views/shared/icons/_icon_status_success.svg
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
index cb785635b7e..cb785635b7e 100755..100644
--- a/app/views/shared/icons/_icon_status_warning.svg
+++ b/app/views/shared/icons/_icon_status_warning.svg
diff --git a/app/views/shared/icons/_key_2.svg b/app/views/shared/icons/_key_2.svg
deleted file mode 100644
index 368b2876c60..00000000000
--- a/app/views/shared/icons/_key_2.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></svg>
diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg
deleted file mode 100644
index 7267462418e..00000000000
--- a/app/views/shared/icons/_thumbs_up.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg>
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index f16bc8dd430..9ef015047c9 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -3,9 +3,9 @@
- button_method = issuable_close_reopen_button_method(issuable)
- if can_update && is_current_user
- = link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
+ = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
- = link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method,
+ = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index a38cd319e3c..39a5171c1d6 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -7,7 +7,7 @@
- button_method = issuable_close_reopen_button_method(issuable)
.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
- = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable),
+ = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
@@ -16,7 +16,7 @@
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
- data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable),
+ data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
@@ -26,7 +26,7 @@
= display_issuable_type
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
- data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable),
+ data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_path(issuable),
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c4ed7f6e750..d3f0aa2d339 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -11,13 +11,13 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- 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), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.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/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 8a71819aa8e..d2b62557e03 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -11,7 +11,7 @@
.hide-collapsed.participants-list
- participants.each do |participant|
.participants-author.js-participants-author
- = link_to_member(@project, participant, name: false, size: 24)
+ = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- if participants_extra > 0
.hide-collapsed.participants-more
%a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0afa48b392c..7b7411b1e23 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -24,9 +24,9 @@
.block.milestone
.sidebar-collapsed-icon
= icon('clock-o', 'aria-hidden': 'true')
- %span
+ %span.milestone-title
- if issuable.milestone
- %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
@@ -37,7 +37,7 @@
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value None
@@ -119,6 +119,10 @@
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
+ - if issuable.has_attribute?(:discussion_locked)
+ %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
+ #js-lock-entry-point
+
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- subscribed = issuable.subscribed?(current_user, @project)
diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml
index 48d04678d47..4a3547e9e70 100644
--- a/app/views/shared/issuable/_user_dropdown_item.html.haml
+++ b/app/views/shared/issuable/_user_dropdown_item.html.haml
@@ -4,7 +4,7 @@
%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
%button.btn.btn-link.dropdown-user{ type: :button }
.avatar-container.s40
- = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false).gsub('/images/{{avatar_url}}','{{avatar_url}}').html_safe
+ = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false)
.dropdown-user-details
%span
= user.name
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 3739f4c221d..14395bcc661 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -26,6 +26,6 @@
%span.assignee-icon
- assignees.each do |assignee|
- = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
+ = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 1dfe380db16..4b9af78bc1a 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -7,7 +7,7 @@
= button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
= icon('caret-down', class: 'toggle-icon')
- %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
+ %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 725bf916592..71c0d740bc8 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -24,20 +24,21 @@
-# DiffNote
= f.hidden_field :position
- = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
- = render 'projects/zen', f: f,
- attr: :note,
- classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
- supports_quick_actions: supports_quick_actions,
- supports_autocomplete: supports_autocomplete
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
- .error-alert
-
- .note-form-actions.clearfix
- = render partial: 'shared/notes/comment_button'
-
- = yield(:note_actions)
-
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
- Discard draft
+ .discussion-form-container
+ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render 'projects/zen', f: f,
+ attr: :note,
+ classes: 'note-textarea js-note-text',
+ placeholder: "Write a comment or drag your files here...",
+ supports_quick_actions: supports_quick_actions,
+ supports_autocomplete: supports_autocomplete
+ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ .error-alert
+
+ .note-form-actions.clearfix
+ = render partial: 'shared/notes/comment_button'
+
+ = yield(:note_actions)
+
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ Discard draft
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 4f00a9f2759..b6085fd3af0 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -1,7 +1,10 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
+- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
- note_editable = note_editable?(note)
+- note_counter = local_assigns.fetch(:note_counter, 0)
+
%li.timeline-entry{ id: dom_id(note),
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
@@ -12,8 +15,18 @@
- if note.system
= icon_for_system_note(note)
- else
- %a{ href: user_path(note.author) }
+ %a.image-diff-avatar-link{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
+ - if note.is_a?(DiffNote) && note.on_image?
+ - if show_image_comment_badge && note_counter == 0
+ -# Only show this for the first comment in the discussion
+ %span.image-comment-badge.inverted
+ = icon('comment-o')
+ - elsif note_counter == 0
+ - counter = badge_counter if local_assigns[:badge_counter]
+ - badge_class = "hidden" if @fresh_discussion || counter.nil?
+ %span.badge{ class: badge_class }
+ = counter
.timeline-content
.note-header
.note-header-info
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index e3e86709b8f..c6e18108c7a 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,3 +1,6 @@
+- issuable = @issue || @merge_request
+- discussion_locked = issuable&.discussion_locked?
+
%ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes"
@@ -21,5 +24,14 @@
or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
to comment
-
+- elsif discussion_locked
+ .disabled-comment.text-center.prepend-top-default
+ %span.issuable-note-warning
+ %span.icon= sprite_icon('lock', size: 14)
+ %span
+ This
+ = issuable.class.to_s.titleize.downcase
+ is locked. Only
+ %b project members
+ can comment.
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 093b2d82813..79e8f8d0e89 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -6,15 +6,15 @@
%script#js-register-u2f-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled?
.row.append-bottom-10
- .col-md-3
- %button#js-setup-u2f-device.btn.btn-info Setup new U2F device
- .col-md-9
+ .col-md-4
+ %button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device
+ .col-md-8
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
- else
.row.append-bottom-10
- .col-md-3
- %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup new U2F device
- .col-md-9
+ .col-md-4
+ %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device
+ .col-md-8
%p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
%script#js-register-u2f-in-progress{ type: "text/template" }
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 879e0f99b14..6c3cd6ecefe 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -9,7 +9,7 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block.user-cover-block.layout-nav
+ .cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
@@ -99,8 +99,6 @@
Snippets
%div{ class: container_class }
- - if @user == current_user && show_callout?('user_callout_dismissed')
- = render 'shared/user_callout'
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index e2a1b3dcc41..52e7d346e74 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -6,6 +6,7 @@ class BuildFinishedWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
+ BuildTraceSectionsWorker.perform_async(build.id)
BuildCoverageWorker.new.perform(build.id)
BuildHooksWorker.new.perform(build.id)
end
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
new file mode 100644
index 00000000000..8c57e8f767b
--- /dev/null
+++ b/app/workers/build_trace_sections_worker.rb
@@ -0,0 +1,8 @@
+class BuildTraceSectionsWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id)&.parse_trace_sections!
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
new file mode 100644
index 00000000000..63300b58a25
--- /dev/null
+++ b/app/workers/cluster_provision_worker.rb
@@ -0,0 +1,10 @@
+class ClusterProvisionWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+
+ def perform(cluster_id)
+ Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
+ Ci::ProvisionClusterService.new.execute(cluster)
+ end
+ end
+end
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
new file mode 100644
index 00000000000..a5074d13220
--- /dev/null
+++ b/app/workers/concerns/cluster_queue.rb
@@ -0,0 +1,10 @@
+##
+# Concern for setting Sidekiq settings for the various Gcp clusters workers.
+#
+module ClusterQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :gcp_cluster
+ end
+end
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
new file mode 100644
index 00000000000..ca276d7801c
--- /dev/null
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -0,0 +1,11 @@
+class ProjectMigrateHashedStorageWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id)
+ project = Project.find_by(id: project_id)
+ return if project.nil? || project.pending_delete?
+
+ ::Projects::HashedStorageMigrationService.new(project, logger).execute
+ end
+end
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
new file mode 100644
index 00000000000..b48ead799b9
--- /dev/null
+++ b/app/workers/storage_migrator_worker.rb
@@ -0,0 +1,30 @@
+class StorageMigratorWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ BATCH_SIZE = 100
+
+ def perform(start, finish)
+ projects = build_relation(start, finish)
+
+ projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
+ Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
+
+ begin
+ project.migrate_to_hashed_storage!
+ rescue => err
+ Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
+ end
+ end
+ end
+
+ def build_relation(start, finish)
+ relation = Project
+ table = Project.arel_table
+
+ relation = relation.where(table[:id].gteq(start)) if start
+ relation = relation.where(table[:id].lteq(finish)) if finish
+
+ relation
+ end
+end
diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb
deleted file mode 100644
index c9d382cc5d6..00000000000
--- a/app/workers/use_key_worker.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class UseKeyWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- def perform(key_id)
- key = Key.find(key_id)
- key.touch(:last_used_at)
- rescue ActiveRecord::RecordNotFound
- Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
-
- false
- end
-end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
new file mode 100644
index 00000000000..5aa3bbdaa9d
--- /dev/null
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -0,0 +1,27 @@
+class WaitForClusterCreationWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(cluster_id)
+ Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
+ Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
+ case operation.status
+ when 'RUNNING'
+ if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
+ return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
+ when 'DONE'
+ Ci::FinalizeClusterCreationService.new.execute(cluster)
+ else
+ return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/12673-fix_v3_project_hooks_build_events b/changelogs/unreleased/12673-fix_v3_project_hooks_build_events
deleted file mode 100644
index 59bc646406f..00000000000
--- a/changelogs/unreleased/12673-fix_v3_project_hooks_build_events
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Fix v3 api project_hooks POST and PUT operations for build_events"
-merge_request: 12673
-author: Richard Clamp
diff --git a/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md b/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md
deleted file mode 100644
index 87e95240bba..00000000000
--- a/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "reset text-align to initial to let elements with dir="auto" align texts to right in RTL languages ( default is left )"
-merge_request: 12892
-author: goshhob
diff --git a/changelogs/unreleased/12968-generalize-profile-updates.yml b/changelogs/unreleased/12968-generalize-profile-updates.yml
deleted file mode 100644
index d09793512c1..00000000000
--- a/changelogs/unreleased/12968-generalize-profile-updates.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Generalize profile updates from providers
-merge_request: 12968
-author: Alexandros Keramidas
diff --git a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml
deleted file mode 100644
index 90b169390d2..00000000000
--- a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: disabling notifications globally now properly turns off group/project added
- emails
-merge_request: 13325
-author: @jneen
-type: fixed
diff --git a/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml b/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml
new file mode 100644
index 00000000000..5f98d0cc766
--- /dev/null
+++ b/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml
@@ -0,0 +1,5 @@
+---
+title: Confirmation email shows link as text instead of human readable text
+merge_request: 14243
+author: bitsapien
+type: changed
diff --git a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml
new file mode 100644
index 00000000000..607a8683aff
--- /dev/null
+++ b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml
@@ -0,0 +1,6 @@
+---
+title: Allow to use same periods for different housekeeping tasks (effectively
+ skipping the lesser task)
+merge_request: 13711
+author: cernvcs
+type: added
diff --git a/changelogs/unreleased/14395-upgrade-gitlab-markup.yml b/changelogs/unreleased/14395-upgrade-gitlab-markup.yml
new file mode 100644
index 00000000000..d1f90fe5eb1
--- /dev/null
+++ b/changelogs/unreleased/14395-upgrade-gitlab-markup.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade gitlab-markup gem
+merge_request: 14395
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/14553-missing-space-in-log-msg.yml b/changelogs/unreleased/14553-missing-space-in-log-msg.yml
new file mode 100644
index 00000000000..a0420d49770
--- /dev/null
+++ b/changelogs/unreleased/14553-missing-space-in-log-msg.yml
@@ -0,0 +1,5 @@
+---
+title: "Add missing space in Sidekiq memory killer log message"
+merge_request: 14553
+author: Benjamin Drung
+type: fixed
diff --git a/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml b/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml
deleted file mode 100644
index 8ec78bbd41f..00000000000
--- a/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add settings for minimum SSH key strength and allowed key type
-merge_request: 13712
-author: Cory Hinshaw
-type: added
diff --git a/changelogs/unreleased/18308-escape-characters.yml b/changelogs/unreleased/18308-escape-characters.yml
new file mode 100644
index 00000000000..8766e971490
--- /dev/null
+++ b/changelogs/unreleased/18308-escape-characters.yml
@@ -0,0 +1,5 @@
+---
+title: Escape quotes in git username
+merge_request: 14020
+author: Brandon Everett
+type: fixed
diff --git a/changelogs/unreleased/18608-lock-issues.yml b/changelogs/unreleased/18608-lock-issues.yml
new file mode 100644
index 00000000000..7d907f744f6
--- /dev/null
+++ b/changelogs/unreleased/18608-lock-issues.yml
@@ -0,0 +1,4 @@
+title: Discussion lock for issues and merge requests
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml b/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml
deleted file mode 100644
index 6d5baa8c10f..00000000000
--- a/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide admin link from default search results for non-admins
-merge_request: 14015
-author:
-type: fixed
diff --git a/changelogs/unreleased/20049-projects-api-forks.yml b/changelogs/unreleased/20049-projects-api-forks.yml
new file mode 100644
index 00000000000..c6470620f57
--- /dev/null
+++ b/changelogs/unreleased/20049-projects-api-forks.yml
@@ -0,0 +1,5 @@
+---
+title: Add an API endpoint to determine the forks of a project
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
new file mode 100644
index 00000000000..245b8129de8
--- /dev/null
+++ b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
@@ -0,0 +1,5 @@
+---
+title: Return only group's members in user dropdowns on issuables list pages
+merge_request: 14249
+author:
+type: changed
diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
new file mode 100644
index 00000000000..469cc04930b
--- /dev/null
+++ b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
@@ -0,0 +1,5 @@
+---
+title: Make the labels in the Compare form less confusing
+merge_request: 14225
+author:
+type: changed
diff --git a/changelogs/unreleased/21949-add-type-to-changelog.yml b/changelogs/unreleased/21949-add-type-to-changelog.yml
deleted file mode 100644
index a20f6b7ad4e..00000000000
--- a/changelogs/unreleased/21949-add-type-to-changelog.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added type to CHANGELOG entries
-merge_request:
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
deleted file mode 100644
index f4011b756a5..00000000000
--- a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address
-merge_request: 6597
-author:
diff --git a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
deleted file mode 100644
index abb9e33d626..00000000000
--- a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes Sortable default scope.
-merge_request: 13558
-author:
-type: fixed
diff --git a/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml b/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml
new file mode 100644
index 00000000000..36bed037160
--- /dev/null
+++ b/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show an "Unsubscribe" link in snippet comment notifications
+merge_request: 14764
+author:
+type: fixed
diff --git a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
new file mode 100644
index 00000000000..e43e97303e2
--- /dev/null
+++ b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
@@ -0,0 +1,5 @@
+---
+title: Extract AutocompleteController#users into finder
+merge_request: 13778
+author: Maxim Rydkin, Mayra Cabrera
+type: other
diff --git a/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml b/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml
deleted file mode 100644
index fa1ca3d25b2..00000000000
--- a/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add CI/CD job predefined variables with user name and login
-merge_request: 13824
-author:
-type: added
diff --git a/changelogs/unreleased/26890-fix-default-branches-sorting.yml b/changelogs/unreleased/26890-fix-default-branches-sorting.yml
new file mode 100644
index 00000000000..cf7060190b3
--- /dev/null
+++ b/changelogs/unreleased/26890-fix-default-branches-sorting.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the default branches sorting to actually be 'Last updated'
+merge_request: 14295
+author:
+type: fixed
diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
deleted file mode 100644
index 0e8f7093b34..00000000000
--- a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor Timelogs structure to use foreign keys.
-merge_request: 8769
-author:
diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml
deleted file mode 100644
index ed38fd37103..00000000000
--- a/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease ABC threshold to 55.25
-merge_request: 13904
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/28283-uuid-storage.yml b/changelogs/unreleased/28283-uuid-storage.yml
deleted file mode 100644
index 283e06d4b7f..00000000000
--- a/changelogs/unreleased/28283-uuid-storage.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hashed Storage support for Repositories (EXPERIMENTAL)
-merge_request: 13246
-author:
diff --git a/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml b/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml
deleted file mode 100644
index 129cf505a3f..00000000000
--- a/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add time stats to Issue and Merge Request API
-merge_request: 13335
-author: @travismiller
diff --git a/changelogs/unreleased/28938-password-change-workflow-for-admins.yml b/changelogs/unreleased/28938-password-change-workflow-for-admins.yml
deleted file mode 100644
index 0781e1a2fce..00000000000
--- a/changelogs/unreleased/28938-password-change-workflow-for-admins.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changes the password change workflow for admins.
-merge_request: 13901
-author:
-type: fixed
diff --git a/changelogs/unreleased/29811-fix-line-number-alignment.yml b/changelogs/unreleased/29811-fix-line-number-alignment.yml
deleted file mode 100644
index 94b3328a7f2..00000000000
--- a/changelogs/unreleased/29811-fix-line-number-alignment.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix the alignment of line numbers to lines of code in code viewer
-merge_request: 13403
-author: Trevor Flynn \ No newline at end of file
diff --git a/changelogs/unreleased/30162-retire-koding-integration.yml b/changelogs/unreleased/30162-retire-koding-integration.yml
deleted file mode 100644
index 63c2b9eb161..00000000000
--- a/changelogs/unreleased/30162-retire-koding-integration.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Deprecation of Koding integration, removal of setting in Admin Panel
-merge_request: 13992
-author: @mydigitalself
diff --git a/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml b/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml
deleted file mode 100644
index 4d21717e161..00000000000
--- a/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Ensure correct visibility level options shown on all Project, Group, and Snippets
- forms
-merge_request: 13442
-author:
-type: fixed
diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
new file mode 100644
index 00000000000..6036e1a43a0
--- /dev/null
+++ b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Perceived Complexity threshold to 15
+merge_request: 14160
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
deleted file mode 100644
index 4a8d8097169..00000000000
--- a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease Cyclomatic Complexity threshold to 14
-merge_request: 13972
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
new file mode 100644
index 00000000000..a404456198a
--- /dev/null
+++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Cyclomatic Complexity threshold to 13
+merge_request: 14152
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml b/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml
deleted file mode 100644
index 06e8180db64..00000000000
--- a/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix group and project search for anonymous users
-merge_request: 13745
-author:
-type: fixed
diff --git a/changelogs/unreleased/31470-fix-api-files-raw.yml b/changelogs/unreleased/31470-fix-api-files-raw.yml
deleted file mode 100644
index 271a945a998..00000000000
--- a/changelogs/unreleased/31470-fix-api-files-raw.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the /projects/:id/repository/files/:file_path/raw endpoint to handle dots in the file_path
-merge_request: 13512
-author: mahcsig
-type: fixed
diff --git a/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml b/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
new file mode 100644
index 00000000000..6110e245013
--- /dev/null
+++ b/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
@@ -0,0 +1,5 @@
+---
+title: Added defaults for protected branches dropdowns on the repository settings
+merge_request: 14278
+author:
+type: changed
diff --git a/changelogs/unreleased/32340-correct-jobs-api-documentation b/changelogs/unreleased/32340-correct-jobs-api-documentation
deleted file mode 100644
index 4ada62356eb..00000000000
--- a/changelogs/unreleased/32340-correct-jobs-api-documentation
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Correction to documention for manual steps on the Jobs API"
-merge_request: 11411
-author: Zac Sturgess \ No newline at end of file
diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
deleted file mode 100644
index fde70c47ca6..00000000000
--- a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Redesign project feature permissions settings
-merge_request: 14062
-author:
-type: changed
diff --git a/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml b/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml
new file mode 100644
index 00000000000..d3aac241b75
--- /dev/null
+++ b/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml
@@ -0,0 +1,5 @@
+---
+title: Adds gitlab features and components to usage ping data.
+merge_request: 14305
+author:
+type: other
diff --git a/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml b/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml
new file mode 100644
index 00000000000..727f3cecd52
--- /dev/null
+++ b/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml
@@ -0,0 +1,5 @@
+---
+title: Link SAML users to LDAP by email.
+merge_request: 14216
+author:
+type: changed
diff --git a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml
deleted file mode 100644
index 278ef2a8acb..00000000000
--- a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added tests for commits API unauthenticated user and public/private project
-merge_request: 13287
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml b/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
new file mode 100644
index 00000000000..ce83b140eb6
--- /dev/null
+++ b/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Add online view of HTML artifacts for public projects
+merge_request: 14399
+author:
+type: added
diff --git a/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml b/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
new file mode 100644
index 00000000000..8260f7fa4b2
--- /dev/null
+++ b/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes project denial of service via gitmodules using Extended ASCII.
+merge_request: 14301
+author:
+type: fixed
diff --git a/changelogs/unreleased/34261-move-move-to-sidebar.yml b/changelogs/unreleased/34261-move-move-to-sidebar.yml
deleted file mode 100644
index 59fa1d4c221..00000000000
--- a/changelogs/unreleased/34261-move-move-to-sidebar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move "Move issue" controls to right-sidebar
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml b/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml
new file mode 100644
index 00000000000..d34e685b5f5
--- /dev/null
+++ b/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml
@@ -0,0 +1,5 @@
+---
+title: Load sidebar participants avatars only when visible
+merge_request: 14270
+author:
+type: other
diff --git a/changelogs/unreleased/34371-cycle-analitcs-global.yml b/changelogs/unreleased/34371-cycle-analitcs-global.yml
new file mode 100644
index 00000000000..5e9f0a85e9a
--- /dev/null
+++ b/changelogs/unreleased/34371-cycle-analitcs-global.yml
@@ -0,0 +1,5 @@
+---
+title: Removes cycle analytics service and store from global namespace
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml
deleted file mode 100644
index 7de30d82601..00000000000
--- a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Improves performance of vue code by using vue files and moving svg out of data
- function in pipeline schedule callout
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml b/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml
deleted file mode 100644
index d33b55ef681..00000000000
--- a/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move ConvDev Index location to after Cohorts.
-merge_request: !13398
-author:
diff --git a/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml b/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml
deleted file mode 100644
index a61d703bacd..00000000000
--- a/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves markdown rendering performance for commit lists.
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/34510-board-issues-sql-speedup.yml b/changelogs/unreleased/34510-board-issues-sql-speedup.yml
new file mode 100644
index 00000000000..244ff7e9dfa
--- /dev/null
+++ b/changelogs/unreleased/34510-board-issues-sql-speedup.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize the boards' issues fetching.
+merge_request: 14198
+author:
+type: other
diff --git a/changelogs/unreleased/34643-fix-project-path-slugify.yml b/changelogs/unreleased/34643-fix-project-path-slugify.yml
deleted file mode 100644
index f7018a1aca5..00000000000
--- a/changelogs/unreleased/34643-fix-project-path-slugify.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix CI_PROJECT_PATH_SLUG slugify
-merge_request: 13350
-author: Ivan Chernov
diff --git a/changelogs/unreleased/34945-readme-div-id.yml b/changelogs/unreleased/34945-readme-div-id.yml
deleted file mode 100644
index c7d26b746b4..00000000000
--- a/changelogs/unreleased/34945-readme-div-id.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add div id to the readme in the project overview
-merge_request: 13735
-author: Riccardo Padovani @rpadovani
-type: added
diff --git a/changelogs/unreleased/34990-top-buttons-misaligned.yml b/changelogs/unreleased/34990-top-buttons-misaligned.yml
deleted file mode 100644
index db60f83ed71..00000000000
--- a/changelogs/unreleased/34990-top-buttons-misaligned.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes margins on the top buttons of the pipeline table
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/35010-projects-nav-dropdown.yml b/changelogs/unreleased/35010-projects-nav-dropdown.yml
deleted file mode 100644
index c5bed723f55..00000000000
--- a/changelogs/unreleased/35010-projects-nav-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add dropdown to Projects nav item
-merge_request: 13866
-author:
-type: added
diff --git a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml
deleted file mode 100644
index 6cd7f4e9cc6..00000000000
--- a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove project select dropdown from breadcrumb
-merge_request: 14010
-author:
-type: changed
diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
deleted file mode 100644
index 74aa337a18c..00000000000
--- a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add option in preferences to change navigation theme color
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/35048-empty-badges.yml b/changelogs/unreleased/35048-empty-badges.yml
deleted file mode 100644
index 816fe82887c..00000000000
--- a/changelogs/unreleased/35048-empty-badges.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevents rendering empty badges when request fails
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/35161_first_time_contributor_badge.yml b/changelogs/unreleased/35161_first_time_contributor_badge.yml
deleted file mode 100644
index f3ab2d9db31..00000000000
--- a/changelogs/unreleased/35161_first_time_contributor_badge.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "First-time contributor badge"
-merge_request: 13143
-author: Micaël Bergeron <micaelbergeron@gmail.com>
diff --git a/changelogs/unreleased/3523-i18n-autodevops.yml b/changelogs/unreleased/3523-i18n-autodevops.yml
new file mode 100644
index 00000000000..10cb22b42a0
--- /dev/null
+++ b/changelogs/unreleased/3523-i18n-autodevops.yml
@@ -0,0 +1,5 @@
+---
+title: Improves i18n for Auto Devops callout
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/35290_allow_public_project_apis.yml b/changelogs/unreleased/35290_allow_public_project_apis.yml
new file mode 100644
index 00000000000..1968eee0a53
--- /dev/null
+++ b/changelogs/unreleased/35290_allow_public_project_apis.yml
@@ -0,0 +1,4 @@
+---
+title: made read-only APIs for public merge requests available without authentication
+merge_request: 13291
+author: haseebeqx
diff --git a/changelogs/unreleased/35343-inherit-milestones-and-labels.yml b/changelogs/unreleased/35343-inherit-milestones-and-labels.yml
deleted file mode 100644
index ce737a67356..00000000000
--- a/changelogs/unreleased/35343-inherit-milestones-and-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: inherits milestone and labels when a merge request is created from issue
-merge_request: 13461
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/35441-fix-division-by-zero.yml b/changelogs/unreleased/35441-fix-division-by-zero.yml
deleted file mode 100644
index 335b2d40494..00000000000
--- a/changelogs/unreleased/35441-fix-division-by-zero.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix division by zero error in blame age mapping
-merge_request: 13803
-author: Jeff Stubler
-type: fixed
diff --git a/changelogs/unreleased/35686-unescape-wiki-title.yml b/changelogs/unreleased/35686-unescape-wiki-title.yml
deleted file mode 100644
index 4b2b7078163..00000000000
--- a/changelogs/unreleased/35686-unescape-wiki-title.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unescape HTML characters in Wiki title
-merge_request: 13942
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/35721-auth-style-confirmation.yml b/changelogs/unreleased/35721-auth-style-confirmation.yml
deleted file mode 100644
index 9963f76e845..00000000000
--- a/changelogs/unreleased/35721-auth-style-confirmation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: restyling of OAuth authorization confirmation
-merge_request:
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/35793_fix_predicate_names.yml b/changelogs/unreleased/35793_fix_predicate_names.yml
deleted file mode 100644
index d4da177dc2e..00000000000
--- a/changelogs/unreleased/35793_fix_predicate_names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove `is_` prefix from predicate method names
-merge_request: 13810
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/35811-copy-link-note.yml b/changelogs/unreleased/35811-copy-link-note.yml
deleted file mode 100644
index 9fa74884c8a..00000000000
--- a/changelogs/unreleased/35811-copy-link-note.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for copying permalink to notes via more actions dropdown
-merge_request: 13299
-author:
-type: added
diff --git a/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml b/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml
deleted file mode 100644
index eac8dbe23c2..00000000000
--- a/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves subgroup creation permissions
-merge_request: 13418
-author:
-type: bugifx
diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml
new file mode 100644
index 00000000000..e7cad5a11d5
--- /dev/null
+++ b/changelogs/unreleased/35917_create_services_for_keys.yml
@@ -0,0 +1,4 @@
+---
+title: creation of keys moved to services
+merge_request: 13331
+author: haseebeqx
diff --git a/changelogs/unreleased/35942-api-binary-encoding.yaml b/changelogs/unreleased/35942-api-binary-encoding.yaml
deleted file mode 100644
index 4f7960d860e..00000000000
--- a/changelogs/unreleased/35942-api-binary-encoding.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: "Fix API to serve binary diffs that are treated as text."
-merge_request: 14038
diff --git a/changelogs/unreleased/35994-archived-projects-only.yml b/changelogs/unreleased/35994-archived-projects-only.yml
deleted file mode 100644
index ce565b177d0..00000000000
--- a/changelogs/unreleased/35994-archived-projects-only.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add an option to list only archived projects
-merge_request: 13492
-author: Mehdi Lahmam (@mehlah)
-type: added
diff --git a/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml b/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml
deleted file mode 100644
index 04791e09b84..00000000000
--- a/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add checks for branch existence before changing HEAD
-merge_request: 13359
-author: Vitaliy @blackst0ne Klachkov
diff --git a/changelogs/unreleased/36041-notification-title.yml b/changelogs/unreleased/36041-notification-title.yml
deleted file mode 100644
index 7c5e0a0cd0d..00000000000
--- a/changelogs/unreleased/36041-notification-title.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't escape html entities in InlineDiffMarkdownMarker
-merge_request:
-author:
diff --git a/changelogs/unreleased/36087-users-cannot-delete-their-account.yml b/changelogs/unreleased/36087-users-cannot-delete-their-account.yml
deleted file mode 100644
index 9ba75d8b1d0..00000000000
--- a/changelogs/unreleased/36087-users-cannot-delete-their-account.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: allow all users to delete their account
-merge_request: 13636
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml b/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml
deleted file mode 100644
index 1b664efb8c2..00000000000
--- a/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Present enqueued merge jobs as Merging as well
-merge_request:
-author:
diff --git a/changelogs/unreleased/36119-issuable-workers.yml b/changelogs/unreleased/36119-issuable-workers.yml
deleted file mode 100644
index beb01ae5b1a..00000000000
--- a/changelogs/unreleased/36119-issuable-workers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Simplify checking if objects exist code in new issaubles workers
-merge_request:
-author:
diff --git a/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml
new file mode 100644
index 00000000000..cea6cb2e48b
--- /dev/null
+++ b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml
@@ -0,0 +1,5 @@
+---
+title: Re-arrange <script> tags before <template> tags in .vue files
+merge_request: 14671
+author:
+type: changed
diff --git a/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml b/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml
deleted file mode 100644
index b51b5e58b39..00000000000
--- a/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Include the `is_admin` field in the `GET /users/:id` API when current user
- is an admin
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml b/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
new file mode 100644
index 00000000000..a820ecee7d2
--- /dev/null
+++ b/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
@@ -0,0 +1,5 @@
+---
+title: Allow prometheus graphs to correctly handle NaN values
+merge_request: 14741
+author:
+type: fixed
diff --git a/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml b/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml
deleted file mode 100644
index 356857d6e8a..00000000000
--- a/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Merge request reference in merge commit changed to full reference
-merge_request: 13518
-author: haseebeqx
-type: fixed
diff --git a/changelogs/unreleased/36385-pipeline-graph-dropdown.yml b/changelogs/unreleased/36385-pipeline-graph-dropdown.yml
deleted file mode 100644
index 1a43c66debd..00000000000
--- a/changelogs/unreleased/36385-pipeline-graph-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevents jobs dropdown from closing in pipeline graph
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
new file mode 100644
index 00000000000..f5ccb163d98
--- /dev/null
+++ b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
@@ -0,0 +1,5 @@
+---
+title: Allow the git circuit breaker to correctly handle missing repository storages
+merge_request: 14417
+author:
+type: fixed
diff --git a/changelogs/unreleased/36611-error-in-getcomposer-link.yml b/changelogs/unreleased/36611-error-in-getcomposer-link.yml
deleted file mode 100644
index 1ff6ec01684..00000000000
--- a/changelogs/unreleased/36611-error-in-getcomposer-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix external link to Composer website
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml b/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
new file mode 100644
index 00000000000..a2e1d07158b
--- /dev/null
+++ b/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
@@ -0,0 +1,6 @@
+---
+title: Reschedule merge request diff background migrations to catch failures from
+ 9.5 run
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml
deleted file mode 100644
index 76c9c905590..00000000000
--- a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Normalize styles for empty state combo button
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36670-remove-edit-form.yml b/changelogs/unreleased/36670-remove-edit-form.yml
new file mode 100644
index 00000000000..4e80b685f67
--- /dev/null
+++ b/changelogs/unreleased/36670-remove-edit-form.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the ability to visit the issue edit form directly
+merge_request: 14523
+author:
+type: removed
diff --git a/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml
new file mode 100644
index 00000000000..3d3efcdbcc6
--- /dev/null
+++ b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Hide close MR button after merge without reloading page
+merge_request: 14122
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml b/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml
deleted file mode 100644
index be08da0433a..00000000000
--- a/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Never wait for sidekiq jobs when creating projects
-merge_request: 13775
-author:
-type: other
diff --git a/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml b/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml
deleted file mode 100644
index a37de4325bb..00000000000
--- a/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unwanted refs after importing a project
-merge_request: 13766
-author:
-type: other
diff --git a/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml b/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml
deleted file mode 100644
index 54c7a8c8788..00000000000
--- a/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix new navigation wrapping and causing height to grow
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml b/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
new file mode 100644
index 00000000000..ee6a7287e86
--- /dev/null
+++ b/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for GPG subkeys in signature verification
+merge_request: 14517
+author:
+type: added
diff --git a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
deleted file mode 100644
index e48a5704fdd..00000000000
--- a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update gpg documentation with gpg2
-merge_request: 13851
-author: M M Arif
-type: other
diff --git a/changelogs/unreleased/36860-migrate-issues-author.yml b/changelogs/unreleased/36860-migrate-issues-author.yml
deleted file mode 100644
index 3e9fcc55836..00000000000
--- a/changelogs/unreleased/36860-migrate-issues-author.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate issues authored by deleted user to the Ghost user
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml b/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml
deleted file mode 100644
index a06c84c30e6..00000000000
--- a/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disable GitLab Project Import Button if source disabled
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36884-gitaly-admin-version.yml b/changelogs/unreleased/36884-gitaly-admin-version.yml
new file mode 100644
index 00000000000..0b3b9a205b5
--- /dev/null
+++ b/changelogs/unreleased/36884-gitaly-admin-version.yml
@@ -0,0 +1,5 @@
+---
+title: Add Gitaly version to Admin Dashboard
+merge_request: 14313
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/36917-branch-tooltip.yml b/changelogs/unreleased/36917-branch-tooltip.yml
deleted file mode 100644
index 2d37de50cec..00000000000
--- a/changelogs/unreleased/36917-branch-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds tooltip to the branch name and improves performance
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36937-fix-invite-by-email-text.yml b/changelogs/unreleased/36937-fix-invite-by-email-text.yml
deleted file mode 100644
index 06c6105fab6..00000000000
--- a/changelogs/unreleased/36937-fix-invite-by-email-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix invite by email address duplication
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36939-fix-find-blobs-by-path.yml b/changelogs/unreleased/36939-fix-find-blobs-by-path.yml
deleted file mode 100644
index b48b10049ed..00000000000
--- a/changelogs/unreleased/36939-fix-find-blobs-by-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix searching for files by path
-merge_request: 13798
-author:
-type: fixed
diff --git a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml
new file mode 100644
index 00000000000..9ac4a0ae7f3
--- /dev/null
+++ b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Add GitLab-Pages version to Admin Dashboard
+merge_request: 14040
+author: travismiller
+type: added
diff --git a/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml b/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml
deleted file mode 100644
index 83f6b2d21e1..00000000000
--- a/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add repository toggle for automatically resolving outdated diff discussions
-merge_request: 14053
-author: AshleyDumaine
-type: added
diff --git a/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml
deleted file mode 100644
index fcaa6ec13f8..00000000000
--- a/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove focus styles from dropdown empty links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml
new file mode 100644
index 00000000000..f3118cf0f2f
--- /dev/null
+++ b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed non-UTF-8 valid branch names from causing an error.
+merge_request: 14090
+type: fixed
diff --git a/changelogs/unreleased/37104-fix-graph-date-format.yml b/changelogs/unreleased/37104-fix-graph-date-format.yml
deleted file mode 100644
index f7d39fe8283..00000000000
--- a/changelogs/unreleased/37104-fix-graph-date-format.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix incorrect date/time formatting on prometheus graphs
-merge_request: 13865
-author:
-type: fixed
diff --git a/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
new file mode 100644
index 00000000000..3364b1d46b3
--- /dev/null
+++ b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect X-axis labels in Prometheus graphs
+merge_request: 14258
+author:
+type: fixed
diff --git a/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml b/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml
deleted file mode 100644
index 34161e63c81..00000000000
--- a/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Better align fallback image emojis
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37158-autodevops-banner.yml b/changelogs/unreleased/37158-autodevops-banner.yml
deleted file mode 100644
index f0ffb670102..00000000000
--- a/changelogs/unreleased/37158-autodevops-banner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Created callout for auto devops
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/37179-dashboard-project-dropdown.yml b/changelogs/unreleased/37179-dashboard-project-dropdown.yml
deleted file mode 100644
index 3ef080b8eae..00000000000
--- a/changelogs/unreleased/37179-dashboard-project-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes disabled state from dashboard project button
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml b/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml
deleted file mode 100644
index ef83dc1d10a..00000000000
--- a/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Respect default group visibility when creating a group'
-merge_request: 13903
-author: Robert Schilling
-type: fixed
diff --git a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml
deleted file mode 100644
index 593e74593c4..00000000000
--- a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deprecate custom SSH client configuration for the git user
-merge_request: 13930
-author:
-type: deprecated
diff --git a/changelogs/unreleased/37220-es-modules.yml b/changelogs/unreleased/37220-es-modules.yml
new file mode 100644
index 00000000000..de81fa9e74d
--- /dev/null
+++ b/changelogs/unreleased/37220-es-modules.yml
@@ -0,0 +1,5 @@
+---
+title: Exports common_utils utility functions as modules
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/37229-mr-widget-status-icon.yml b/changelogs/unreleased/37229-mr-widget-status-icon.yml
new file mode 100644
index 00000000000..6d84d1964ca
--- /dev/null
+++ b/changelogs/unreleased/37229-mr-widget-status-icon.yml
@@ -0,0 +1,5 @@
+---
+title: fix merge request widget status icon for failed CI
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
deleted file mode 100644
index d6d21ac4c51..00000000000
--- a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix a wrong `X-Gitlab-Event` header when testing webhooks
-merge_request: 14108
-author:
-type: fixed
diff --git a/changelogs/unreleased/37331-button-MR-widget.yml b/changelogs/unreleased/37331-button-MR-widget.yml
deleted file mode 100644
index 59bc1bd201e..00000000000
--- a/changelogs/unreleased/37331-button-MR-widget.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix buttons with different height in merge request widget
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37335-counter-active-state.yml b/changelogs/unreleased/37335-counter-active-state.yml
new file mode 100644
index 00000000000..a9632201a89
--- /dev/null
+++ b/changelogs/unreleased/37335-counter-active-state.yml
@@ -0,0 +1,5 @@
+---
+title: Add active states to nav bar counters
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
deleted file mode 100644
index 6a955f5715f..00000000000
--- a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make blob viewer for rich contents wider for mobile
-merge_request: 14011
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
new file mode 100644
index 00000000000..bec7da26b1a
--- /dev/null
+++ b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add help text to runner edit: tags should be separated by commas.'
+merge_request:
+author: Brendan O'Leary
+type: added
diff --git a/changelogs/unreleased/37406-success-status-icon.yml b/changelogs/unreleased/37406-success-status-icon.yml
deleted file mode 100644
index faac947f188..00000000000
--- a/changelogs/unreleased/37406-success-status-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken svg in jobs dropdown for success status
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml b/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml
new file mode 100644
index 00000000000..225ab9acc44
--- /dev/null
+++ b/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the number representing the amount of commits related to a push event
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml b/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml
deleted file mode 100644
index 2a8e8a33225..00000000000
--- a/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add description template examples to documentation
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/37552-replace-js-true-with-js.yml b/changelogs/unreleased/37552-replace-js-true-with-js.yml
new file mode 100644
index 00000000000..f7b614a8839
--- /dev/null
+++ b/changelogs/unreleased/37552-replace-js-true-with-js.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace `tag: true` into `:tag` in the specs'
+merge_request: 14653
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/37970-ci-sections-tracking.yml b/changelogs/unreleased/37970-ci-sections-tracking.yml
new file mode 100644
index 00000000000..a9011b22c6c
--- /dev/null
+++ b/changelogs/unreleased/37970-ci-sections-tracking.yml
@@ -0,0 +1,5 @@
+---
+title: Parse and store gitlab-runner timestamped section markers
+merge_request: 14551
+author:
+type: added
diff --git a/changelogs/unreleased/37970-timestamped-ci.yml b/changelogs/unreleased/37970-timestamped-ci.yml
new file mode 100644
index 00000000000..2a4797f069a
--- /dev/null
+++ b/changelogs/unreleased/37970-timestamped-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Strip gitlab-runner section markers in build trace HTML view
+merge_request: 14393
+author:
+type: added
diff --git a/changelogs/unreleased/37999-fix-circuit-breaker.yml b/changelogs/unreleased/37999-fix-circuit-breaker.yml
new file mode 100644
index 00000000000..a75315c4988
--- /dev/null
+++ b/changelogs/unreleased/37999-fix-circuit-breaker.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the filesystem shard health check to check all configured shards
+merge_request: 14341
+author:
+type: fixed
diff --git a/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml b/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
new file mode 100644
index 00000000000..8b3fae2c103
--- /dev/null
+++ b/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
@@ -0,0 +1,6 @@
+---
+title: Move the deployment flag content to the left when deployment marker is near
+ the end
+merge_request: 14514
+author:
+type: fixed
diff --git a/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml b/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
new file mode 100644
index 00000000000..591e542cd17
--- /dev/null
+++ b/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
@@ -0,0 +1,5 @@
+---
+title: Sync up hover and legend data across all graphs for the prometheus dashboard
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38052-use-simple-api-for-projects.yml b/changelogs/unreleased/38052-use-simple-api-for-projects.yml
new file mode 100644
index 00000000000..49c7485861e
--- /dev/null
+++ b/changelogs/unreleased/38052-use-simple-api-for-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Use `simple=true` for projects API in Projects dropdown for better search performance
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
new file mode 100644
index 00000000000..579c247c4c2
--- /dev/null
+++ b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bottom spacing for dropdowns that open upwards
+merge_request: 14535
+author:
+type: fixed
diff --git a/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
new file mode 100644
index 00000000000..df562077fb3
--- /dev/null
+++ b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `rake gitlab:incoming_email:check` and make it report the actual error
+merge_request: 14423
+author:
+type: fixed
diff --git a/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
new file mode 100644
index 00000000000..768e296fcd7
--- /dev/null
+++ b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
@@ -0,0 +1,6 @@
+---
+title: Does not check if an invariant hashed storage path exists on disk when renaming
+ projects.
+merge_request: 14428
+author:
+type: fixed
diff --git a/changelogs/unreleased/38234-reserve-refs-replace.yml b/changelogs/unreleased/38234-reserve-refs-replace.yml
new file mode 100644
index 00000000000..3a5ffbf9db0
--- /dev/null
+++ b/changelogs/unreleased/38234-reserve-refs-replace.yml
@@ -0,0 +1,5 @@
+---
+title: Also reserve refs/replace after importing a project
+merge_request: 14436
+author:
+type: fixed
diff --git a/changelogs/unreleased/38389-allow-merge-without-success.yml b/changelogs/unreleased/38389-allow-merge-without-success.yml
new file mode 100644
index 00000000000..6a37bcc55fc
--- /dev/null
+++ b/changelogs/unreleased/38389-allow-merge-without-success.yml
@@ -0,0 +1,6 @@
+---
+title: Allow merge in MR widget with no pipeline but using "Only allow merge requests
+ to be merged if the pipeline succeeds"
+merge_request: 14633
+author:
+type: fixed
diff --git a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
new file mode 100644
index 00000000000..419e9295d32
--- /dev/null
+++ b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
@@ -0,0 +1,5 @@
+---
+title: Use explicit boolean true attribute for show-disabled-button in Vue files
+merge_request: 14672
+author:
+type: fixed
diff --git a/changelogs/unreleased/38432-fix-notes-type-for-import.yml b/changelogs/unreleased/38432-fix-notes-type-for-import.yml
new file mode 100644
index 00000000000..db8371f4420
--- /dev/null
+++ b/changelogs/unreleased/38432-fix-notes-type-for-import.yml
@@ -0,0 +1,6 @@
+---
+title: Fix notes type created from import. This should fix some missing notes issues
+ from imported projects
+merge_request: 14524
+author:
+type: fixed
diff --git a/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml b/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml
new file mode 100644
index 00000000000..974adb9ed28
--- /dev/null
+++ b/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix navigation dropdown close animation on mobile screens
+merge_request: 14649
+author:
+type: fixed
diff --git a/changelogs/unreleased/38534-minigraph.yml b/changelogs/unreleased/38534-minigraph.yml
new file mode 100644
index 00000000000..eed240eac2d
--- /dev/null
+++ b/changelogs/unreleased/38534-minigraph.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes mini pipeline graph in commit view
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38571-fix-exception-in-raven-report.yml b/changelogs/unreleased/38571-fix-exception-in-raven-report.yml
new file mode 100644
index 00000000000..62e3b8d304c
--- /dev/null
+++ b/changelogs/unreleased/38571-fix-exception-in-raven-report.yml
@@ -0,0 +1,6 @@
+---
+title: Ensure no exception is raised when Raven tries to get the current user in API
+ context
+merge_request: 14580
+author:
+type: fixed
diff --git a/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml b/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml
new file mode 100644
index 00000000000..a203bff8410
--- /dev/null
+++ b/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml
@@ -0,0 +1,5 @@
+---
+title: Fix comment deletion confirmation dialog typo
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml b/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml
new file mode 100644
index 00000000000..49d0671233a
--- /dev/null
+++ b/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml
@@ -0,0 +1,5 @@
+---
+title: Whitelist authorized_keys.lock in the gitlab:check rake task
+merge_request: 14624
+author:
+type: fixed
diff --git a/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml b/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
new file mode 100644
index 00000000000..18b1645d7a9
--- /dev/null
+++ b/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project snippets breadcrumb link
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml b/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
new file mode 100644
index 00000000000..65a66714bcb
--- /dev/null
+++ b/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
@@ -0,0 +1,5 @@
+---
+title: Make tabs on top scrollable on admin dashboard
+merge_request: 14685
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
new file mode 100644
index 00000000000..bbfe5d49a3e
--- /dev/null
+++ b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken Y-axis scaling in some Prometheus graphs
+merge_request: 14693
+author:
+type: fixed
diff --git a/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
new file mode 100644
index 00000000000..44e16512bae
--- /dev/null
+++ b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Move `lib/ci` to `lib/gitlab/ci`
+merge_request: 14078
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml b/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
new file mode 100644
index 00000000000..5f5a61406da
--- /dev/null
+++ b/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add 1000+ counters to job page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
new file mode 100644
index 00000000000..7f098c8f60c
--- /dev/null
+++ b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
@@ -0,0 +1,5 @@
+---
+title: Change index on ci_builds to optimize Jobs Controller
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml b/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml
new file mode 100644
index 00000000000..9a600282716
--- /dev/null
+++ b/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml
@@ -0,0 +1,5 @@
+---
+title: Add index for merge_requests.merge_commit_sha
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/add-filter-by-my-reaction.yml b/changelogs/unreleased/add-filter-by-my-reaction.yml
deleted file mode 100644
index dc1601cf3ee..00000000000
--- a/changelogs/unreleased/add-filter-by-my-reaction.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add my reaction filter to search bar
-merge_request: 12962
-author: Hiroyuki Sato
diff --git a/changelogs/unreleased/add-labels-template-index.yml b/changelogs/unreleased/add-labels-template-index.yml
new file mode 100644
index 00000000000..5f66c4ce181
--- /dev/null
+++ b/changelogs/unreleased/add-labels-template-index.yml
@@ -0,0 +1,5 @@
+---
+title: Add (partial) index on Labels.template
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml b/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml
deleted file mode 100644
index 4c81d21a94b..00000000000
--- a/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added mock deployment and monitoring service with environments fixtures
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-view-replaced-file-link.yml b/changelogs/unreleased/add-view-replaced-file-link.yml
new file mode 100644
index 00000000000..b5a85f2e0f2
--- /dev/null
+++ b/changelogs/unreleased/add-view-replaced-file-link.yml
@@ -0,0 +1,5 @@
+---
+title: Add view replaced file link for image diffs
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/add_closed_at_attribute.yml b/changelogs/unreleased/add_closed_at_attribute.yml
new file mode 100644
index 00000000000..3afb75e8915
--- /dev/null
+++ b/changelogs/unreleased/add_closed_at_attribute.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'closed_at' attribute to Issues API
+merge_request: 14316
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/add_message_to_the_404_page.yml b/changelogs/unreleased/add_message_to_the_404_page.yml
deleted file mode 100644
index f567796fe9f..00000000000
--- a/changelogs/unreleased/add_message_to_the_404_page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed message and title on the 404 page
-merge_request:
-author: Branka Martinovic
-type: added
diff --git a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml
deleted file mode 100644
index 3e4105a4232..00000000000
--- a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add quick submission on user settings page
-merge_request: 14007
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
new file mode 100644
index 00000000000..0470c6519f4
--- /dev/null
+++ b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltip for milestone due date to issue and merge request lists
+merge_request: 14318
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/additional-time-series-charts.yml b/changelogs/unreleased/additional-time-series-charts.yml
deleted file mode 100644
index 80c1af54881..00000000000
--- a/changelogs/unreleased/additional-time-series-charts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added support the multiple time series for prometheus monitoring
-merge_request: !36893
-author:
-type: changed
diff --git a/changelogs/unreleased/adjusting-tooltips.yml b/changelogs/unreleased/adjusting-tooltips.yml
new file mode 100644
index 00000000000..726b75caecd
--- /dev/null
+++ b/changelogs/unreleased/adjusting-tooltips.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust tooltips to adhere to 8px grid and make them more readable
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/api-delete-respect-headers.yml b/changelogs/unreleased/api-delete-respect-headers.yml
deleted file mode 100644
index cfc8fbfdf91..00000000000
--- a/changelogs/unreleased/api-delete-respect-headers.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Respect the "If-Unmodified-Since" header when delting a resource'
-merge_request: 9621
-author: Robert Schilling
-type: added
diff --git a/changelogs/unreleased/api-gpg-key-management.yml b/changelogs/unreleased/api-gpg-key-management.yml
deleted file mode 100644
index 0be35a5823b..00000000000
--- a/changelogs/unreleased/api-gpg-key-management.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Add GPG key management'
-merge_request: 13828
-author: Robert Schilling
-type: added
diff --git a/changelogs/unreleased/api_branches_head.yml b/changelogs/unreleased/api_branches_head.yml
deleted file mode 100644
index 68d8d3d5168..00000000000
--- a/changelogs/unreleased/api_branches_head.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add branch existence check to the APIv4 branches via HEAD request
-merge_request: 13979
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml b/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml
deleted file mode 100644
index fd0b7c4f43c..00000000000
--- a/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fire hooks asynchronously when creating a new job to improve performance
-merge_request: 13734
-author:
-type: changed
diff --git a/changelogs/unreleased/breadcrumb-item-links.yml b/changelogs/unreleased/breadcrumb-item-links.yml
new file mode 100644
index 00000000000..9b66456efca
--- /dev/null
+++ b/changelogs/unreleased/breadcrumb-item-links.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed issue/merge request breadcrumb titles not having links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/breadcrumbs-line-height-padding.yml b/changelogs/unreleased/breadcrumbs-line-height-padding.yml
new file mode 100644
index 00000000000..3ac56c8b593
--- /dev/null
+++ b/changelogs/unreleased/breadcrumbs-line-height-padding.yml
@@ -0,0 +1,5 @@
+---
+title: breadcrumbs receives padding when double lined
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml b/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml
new file mode 100644
index 00000000000..3a99729fb48
--- /dev/null
+++ b/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml
@@ -0,0 +1,5 @@
+---
+title: Show notes number more user-friendly in the graph
+merge_request: 13949
+author: Vladislav Kaverin
+type: changed
diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml
deleted file mode 100644
index 7571999fa75..00000000000
--- a/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bumps omniauth-ldap gem version to 2.0.4
-merge_request: 13465
-author:
diff --git a/changelogs/unreleased/bvl-fork-network-schema.yml b/changelogs/unreleased/bvl-fork-network-schema.yml
new file mode 100644
index 00000000000..97b2d5acada
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-network-schema.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge requests across a fork network
+merge_request: 14422
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-improve-bare-project-import.yml b/changelogs/unreleased/bvl-improve-bare-project-import.yml
deleted file mode 100644
index 74c1da4ea40..00000000000
--- a/changelogs/unreleased/bvl-improve-bare-project-import.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Improve bare project import: Allow subgroups, take default visibility level
- into account'
-merge_request: 13670
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-validate-po-files.yml b/changelogs/unreleased/bvl-validate-po-files.yml
deleted file mode 100644
index f840b2c3973..00000000000
--- a/changelogs/unreleased/bvl-validate-po-files.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Validate PO-files in static analysis
-merge_request: 13000
-author:
diff --git a/changelogs/unreleased/cache-issue-and-mr-counts.yml b/changelogs/unreleased/cache-issue-and-mr-counts.yml
deleted file mode 100644
index fe3fe3be976..00000000000
--- a/changelogs/unreleased/cache-issue-and-mr-counts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache the number of open issues and merge requests
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/change-dashed-border-button-color.yml b/changelogs/unreleased/change-dashed-border-button-color.yml
new file mode 100644
index 00000000000..038bea79273
--- /dev/null
+++ b/changelogs/unreleased/change-dashed-border-button-color.yml
@@ -0,0 +1,5 @@
+---
+title: changed dashed border button color to be darker
+merge_request: !14041
+author:
+type: other
diff --git a/changelogs/unreleased/check-trigger-permissions.yml b/changelogs/unreleased/check-trigger-permissions.yml
deleted file mode 100644
index e0809cea9bf..00000000000
--- a/changelogs/unreleased/check-trigger-permissions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve migrations using triggers
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/close-issue-by-implements.yml b/changelogs/unreleased/close-issue-by-implements.yml
new file mode 100644
index 00000000000..fe36ce3f7aa
--- /dev/null
+++ b/changelogs/unreleased/close-issue-by-implements.yml
@@ -0,0 +1,5 @@
+---
+title: "Add \"implements\" to the default issue closing message regex"
+merge_request: 14612
+author: Guilherme Vieira
+type: added
diff --git a/changelogs/unreleased/collapsable-pipeline-settings.yml b/changelogs/unreleased/collapsable-pipeline-settings.yml
deleted file mode 100644
index d41959f8ab0..00000000000
--- a/changelogs/unreleased/collapsable-pipeline-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add collapsable sections for Pipeline Settings
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/commit-row-avatar-align-top.yml b/changelogs/unreleased/commit-row-avatar-align-top.yml
new file mode 100644
index 00000000000..aa5ab770bd8
--- /dev/null
+++ b/changelogs/unreleased/commit-row-avatar-align-top.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed commit avatars being centered vertically
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml
new file mode 100644
index 00000000000..9e6a429f6f0
--- /dev/null
+++ b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Tooltips in the commit info box now all face the same direction
+merge_request:
+author: Jedidiah Broadbent
+type: fixed
diff --git a/changelogs/unreleased/content-title-link-hover-bg.yml b/changelogs/unreleased/content-title-link-hover-bg.yml
new file mode 100644
index 00000000000..c4c31c2ad06
--- /dev/null
+++ b/changelogs/unreleased/content-title-link-hover-bg.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed navbar title colors leaking out of the navbar
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/declarative-policy-optimisations.yml b/changelogs/unreleased/declarative-policy-optimisations.yml
new file mode 100644
index 00000000000..dc51c89d575
--- /dev/null
+++ b/changelogs/unreleased/declarative-policy-optimisations.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up permission checks
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/disable-project-export.yml b/changelogs/unreleased/disable-project-export.yml
deleted file mode 100644
index d7ca9f46193..00000000000
--- a/changelogs/unreleased/disable-project-export.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to disable project export on instance
-merge_request: 13211
-author: Robin Bobbitt
diff --git a/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml b/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml
new file mode 100644
index 00000000000..057407b78d9
--- /dev/null
+++ b/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml
@@ -0,0 +1,6 @@
+---
+title: Fix bug that caused merge requests with diff notes imported from Bitbucket
+ to raise errors
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-closing-issue-urls.yml b/changelogs/unreleased/dm-closing-issue-urls.yml
new file mode 100644
index 00000000000..059e406b63d
--- /dev/null
+++ b/changelogs/unreleased/dm-closing-issue-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly detect multiple issue URLs after 'Closes...' in MR descriptions
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-copy-parallel-diff.yml b/changelogs/unreleased/dm-copy-parallel-diff.yml
new file mode 100644
index 00000000000..96a65007661
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-parallel-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Only copy old/new code when selecting left/right side of parallel diff
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/dm-pat-revoke.yml b/changelogs/unreleased/dm-pat-revoke.yml
new file mode 100644
index 00000000000..32ac66056d5
--- /dev/null
+++ b/changelogs/unreleased/dm-pat-revoke.yml
@@ -0,0 +1,5 @@
+---
+title: Set default scope on PATs that don't have one set to allow them to be revoked
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-simple-project-avatar-url.yml b/changelogs/unreleased/dm-simple-project-avatar-url.yml
new file mode 100644
index 00000000000..e517345f5d2
--- /dev/null
+++ b/changelogs/unreleased/dm-simple-project-avatar-url.yml
@@ -0,0 +1,5 @@
+---
+title: Expose avatar_url when requesting list of projects from API with simple=true
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
new file mode 100644
index 00000000000..3dfe4114cc9
--- /dev/null
+++ b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Fix docs for lightweight tag creation via API
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml b/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml
new file mode 100644
index 00000000000..3cdcff8caaf
--- /dev/null
+++ b/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml
@@ -0,0 +1,5 @@
+---
+title: Clarify artifact download via the API only accepts branch or tag name for ref
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml b/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml
new file mode 100644
index 00000000000..eea679d0814
--- /dev/null
+++ b/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml
@@ -0,0 +1,5 @@
+---
+title: Change recommended MySQL version to 5.6
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/docs-add-summary-about-project-archiving.yml b/changelogs/unreleased/docs-add-summary-about-project-archiving.yml
new file mode 100644
index 00000000000..cc1b48a682d
--- /dev/null
+++ b/changelogs/unreleased/docs-add-summary-about-project-archiving.yml
@@ -0,0 +1,5 @@
+---
+title: Add documentation to summarise project archiving
+merge_request: 14650
+author:
+type: other
diff --git a/changelogs/unreleased/docs-confidential-issue.yml b/changelogs/unreleased/docs-confidential-issue.yml
deleted file mode 100644
index 841970ef4cf..00000000000
--- a/changelogs/unreleased/docs-confidential-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update documentation for confidential issue
-merge_request: 14117
-author:
-type: other
diff --git a/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml b/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml
deleted file mode 100644
index d75c46313f4..00000000000
--- a/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Document version Group Milestones API introduced
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/docs-fix-15669-issue-move-api.yml b/changelogs/unreleased/docs-fix-15669-issue-move-api.yml
deleted file mode 100644
index db68428fda3..00000000000
--- a/changelogs/unreleased/docs-fix-15669-issue-move-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add to_project_id parameter to Move Issue via API example
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/docs-openid-connect.yml b/changelogs/unreleased/docs-openid-connect.yml
new file mode 100644
index 00000000000..3989ec53cfa
--- /dev/null
+++ b/changelogs/unreleased/docs-openid-connect.yml
@@ -0,0 +1,5 @@
+---
+title: Add link to OpenID Connect documentation
+merge_request: 14368
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml b/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml
deleted file mode 100644
index d8a5073f110..00000000000
--- a/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update 'Using Docker images' documentation
-merge_request: 13848
-author:
-type: other
diff --git a/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml b/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml
deleted file mode 100644
index a7db18dbd60..00000000000
--- a/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed add diff note button not showing after deleting a comment
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ee-add-project-repository-storages-index.yml b/changelogs/unreleased/ee-add-project-repository-storages-index.yml
new file mode 100644
index 00000000000..8a144783cec
--- /dev/null
+++ b/changelogs/unreleased/ee-add-project-repository-storages-index.yml
@@ -0,0 +1,5 @@
+---
+title: Index projects on repository storage
+merge_request: 14414
+author:
+type: other
diff --git a/changelogs/unreleased/events-migration-cleanup.yml b/changelogs/unreleased/events-migration-cleanup.yml
deleted file mode 100644
index 1e3e843f252..00000000000
--- a/changelogs/unreleased/events-migration-cleanup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Finish migration to the new events setup
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml b/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml
new file mode 100644
index 00000000000..d16e052cd92
--- /dev/null
+++ b/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Expose last pipeline details in API response when getting a single commit
+merge_request: 13521
+author: Mehdi Lahmam (@mehlah)
+type: added
diff --git a/changelogs/unreleased/feature-custom-attributes.yml b/changelogs/unreleased/feature-custom-attributes.yml
new file mode 100644
index 00000000000..98736bc8d72
--- /dev/null
+++ b/changelogs/unreleased/feature-custom-attributes.yml
@@ -0,0 +1,4 @@
+---
+title: Support custom attributes on users
+merge_request: 13038
+author: Markus Koller
diff --git a/changelogs/unreleased/feature-dependency-status-badge.yml b/changelogs/unreleased/feature-dependency-status-badge.yml
deleted file mode 100644
index 1becff3585a..00000000000
--- a/changelogs/unreleased/feature-dependency-status-badge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add badge for dependency status
-merge_request: 13588
-author: Markus Koller
-type: other
diff --git a/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml b/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml
deleted file mode 100644
index 920679ca166..00000000000
--- a/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make it possible to download a single job artifact file using the API
-merge_request: 14027
-author:
-type: added
diff --git a/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml b/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml
deleted file mode 100644
index 00c38a0c671..00000000000
--- a/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add CI/CD active kubernetes job policy
-merge_request: 13849
-author:
-type: added
diff --git a/changelogs/unreleased/feature-gpg-verification-status.yml b/changelogs/unreleased/feature-gpg-verification-status.yml
deleted file mode 100644
index 7518fafcdb8..00000000000
--- a/changelogs/unreleased/feature-gpg-verification-status.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Update the GPG verification semantics: A GPG signature must additionally match
- the committer in order to be verified'
-merge_request: 13771
-author: Alexis Reigel
-type: changed
diff --git a/changelogs/unreleased/feature-plantuml-restructured-text.yml b/changelogs/unreleased/feature-plantuml-restructured-text.yml
deleted file mode 100644
index b885029f589..00000000000
--- a/changelogs/unreleased/feature-plantuml-restructured-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add documentation for PlantUML in reStructuredText
-merge_request: 13900
-author: Markus Koller
-type: other
diff --git a/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml b/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml
deleted file mode 100644
index b57b9a3dfbe..00000000000
--- a/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Protected runners
-merge_request: 13194
-author:
-type: added
diff --git a/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml b/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml
deleted file mode 100644
index 969a5aeaed3..00000000000
--- a/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Extend API: Pipeline Schedule Variable'
-merge_request: 13653
-author:
-type: added
diff --git a/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml b/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
new file mode 100644
index 00000000000..14b35b6daee
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
@@ -0,0 +1,5 @@
+---
+title: Create Kubernetes cluster on GKE from k8s service
+merge_request: 14470
+author:
+type: added
diff --git a/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml b/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml
deleted file mode 100644
index 006b0b45844..00000000000
--- a/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implement `failure_reason` on `ci_builds`
-merge_request: 13937
-author:
-type: added
diff --git a/changelogs/unreleased/feature-verify_secondary_emails.yml b/changelogs/unreleased/feature-verify_secondary_emails.yml
new file mode 100644
index 00000000000..e1ecc527f85
--- /dev/null
+++ b/changelogs/unreleased/feature-verify_secondary_emails.yml
@@ -0,0 +1,5 @@
+---
+title: A confirmation email is now sent when adding a secondary email address
+merge_request:
+author: digitalmoksha
+type: added
diff --git a/changelogs/unreleased/ff_port_from_ee.yml b/changelogs/unreleased/ff_port_from_ee.yml
new file mode 100644
index 00000000000..e1cb7804a47
--- /dev/null
+++ b/changelogs/unreleased/ff_port_from_ee.yml
@@ -0,0 +1,5 @@
+---
+title: Move Custom merge methods from EE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/fix-btn-alignment.yml b/changelogs/unreleased/fix-btn-alignment.yml
deleted file mode 100644
index e5dce3d3a0e..00000000000
--- a/changelogs/unreleased/fix-btn-alignment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix inconsistent spacing for edit buttons on issues and merge request page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-edit-merge-request-button-case.yml b/changelogs/unreleased/fix-edit-merge-request-button-case.yml
deleted file mode 100644
index 8550f3e3c1b..00000000000
--- a/changelogs/unreleased/fix-edit-merge-request-button-case.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix edit merge request and issues button inconsistent letter casing
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml b/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml
new file mode 100644
index 00000000000..efb993eff71
--- /dev/null
+++ b/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml
@@ -0,0 +1,5 @@
+---
+title: Fix edit project service cancel button position
+merge_request: 14596
+author: Matt Coleman
+type: fixed
diff --git a/changelogs/unreleased/fix-gpg-case-insensitive.yml b/changelogs/unreleased/fix-gpg-case-insensitive.yml
new file mode 100644
index 00000000000..744ec00a4a8
--- /dev/null
+++ b/changelogs/unreleased/fix-gpg-case-insensitive.yml
@@ -0,0 +1,5 @@
+---
+title: Compare email addresses case insensitively when verifying GPG signatures
+merge_request: 14376
+author: Tim Bishop
+type: fixed
diff --git a/changelogs/unreleased/fix-image-diff-swipe-handle.yml b/changelogs/unreleased/fix-image-diff-swipe-handle.yml
new file mode 100644
index 00000000000..a4e0c2e8465
--- /dev/null
+++ b/changelogs/unreleased/fix-image-diff-swipe-handle.yml
@@ -0,0 +1,5 @@
+---
+title: Fix image diff swipe handle offset to correctly align with the frame
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-import-export-performance.yml b/changelogs/unreleased/fix-import-export-performance.yml
deleted file mode 100644
index 1f59c4eb179..00000000000
--- a/changelogs/unreleased/fix-import-export-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve Import/Export memory usage
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-import-fork-mr.yml b/changelogs/unreleased/fix-import-fork-mr.yml
deleted file mode 100644
index 4e9cf7faae8..00000000000
--- a/changelogs/unreleased/fix-import-fork-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Import/Export issue to do with fork merge requests
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-multi-line-hook-output.yml b/changelogs/unreleased/fix-multi-line-hook-output.yml
new file mode 100644
index 00000000000..f625ec2ee6c
--- /dev/null
+++ b/changelogs/unreleased/fix-multi-line-hook-output.yml
@@ -0,0 +1,5 @@
+---
+title: Display full pre-receive and post-receive hook output in GitLab UI
+merge_request: 14222
+author: Robin Bobbitt
+type: fixed
diff --git a/changelogs/unreleased/fix-npm-security-updates.yml b/changelogs/unreleased/fix-npm-security-updates.yml
deleted file mode 100644
index faa0c3149b8..00000000000
--- a/changelogs/unreleased/fix-npm-security-updates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade brace-expansion NPM package due to security issue
-merge_request: 13665
-author: Markus Koller
-type: security
diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
new file mode 100644
index 00000000000..e0b3851b97f
--- /dev/null
+++ b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed the sidebar scrollbar overlapping links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml
deleted file mode 100644
index ae4df3ee31a..00000000000
--- a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix stray OR in New Project page
-merge_request: 14096
-author: Robin Bobbitt
-type: fixed
diff --git a/changelogs/unreleased/fix-tooltip-width-issue-board.yml b/changelogs/unreleased/fix-tooltip-width-issue-board.yml
new file mode 100644
index 00000000000..a648953c5bd
--- /dev/null
+++ b/changelogs/unreleased/fix-tooltip-width-issue-board.yml
@@ -0,0 +1,5 @@
+---
+title: Issue board tooltips are now the correct width when the column is collapsed
+merge_request:
+author: Jedidiah Broadbent
+type: fixed
diff --git a/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml b/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
new file mode 100644
index 00000000000..c57fceec92f
--- /dev/null
+++ b/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade doorkeeper-openid_connect
+merge_request: 14372
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml b/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml
deleted file mode 100644
index fa50e36e28a..00000000000
--- a/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix typo in the API Deploy Keys documentation page
-merge_request: 14014
-author: Vitaliy @blackst0ne Klachkov
-type: fixed
diff --git a/changelogs/unreleased/fix_wiki_toc_indent.yml b/changelogs/unreleased/fix_wiki_toc_indent.yml
deleted file mode 100644
index 60da2e455f2..00000000000
--- a/changelogs/unreleased/fix_wiki_toc_indent.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Wiki table of contents are now properly nested to reflect header level
-merge_request: 13650
-author: Akihiro Nakashima
-type: fixed
diff --git a/changelogs/unreleased/fl-fix-ca-time-component.yml b/changelogs/unreleased/fl-fix-ca-time-component.yml
new file mode 100644
index 00000000000..ecd377409ca
--- /dev/null
+++ b/changelogs/unreleased/fl-fix-ca-time-component.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typo in cycle analytics breaking time component
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/font-weight-adjusted.yml b/changelogs/unreleased/font-weight-adjusted.yml
deleted file mode 100644
index 827f3485099..00000000000
--- a/changelogs/unreleased/font-weight-adjusted.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed all font-weight values to 400 and 600 and introduced 2 variables to
- manage them
-merge_request: !12896
-author:
diff --git a/changelogs/unreleased/force-two-up-view.yml b/changelogs/unreleased/force-two-up-view.yml
new file mode 100644
index 00000000000..1074eb384bb
--- /dev/null
+++ b/changelogs/unreleased/force-two-up-view.yml
@@ -0,0 +1,5 @@
+---
+title: Force two up view to load by default for image diffs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml
deleted file mode 100644
index 8195e97ed59..00000000000
--- a/changelogs/unreleased/fuzzy-issue-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support a multi-word fuzzy seach issues/merge requests on search bar
-merge_request: 13780
-author: Hiroyuki Sato
-type: changed
diff --git a/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
new file mode 100644
index 00000000000..13ec113167f
--- /dev/null
+++ b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
@@ -0,0 +1,5 @@
+---
+title: Bump google-api-client Gem from 0.8.6 to 0.13.6
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
deleted file mode 100644
index 90c25d782c8..00000000000
--- a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Gitaly feature toggles are on by default in development
-merge_request: 13802
-author:
-type: other
diff --git a/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml b/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml
deleted file mode 100644
index edf11484d1f..00000000000
--- a/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make Gitaly PostUploadPack mandatory
-merge_request: 13953
-author:
-type: changed
diff --git a/changelogs/unreleased/gitaly_feature_flag_metadata.yml b/changelogs/unreleased/gitaly_feature_flag_metadata.yml
new file mode 100644
index 00000000000..58e42ef9324
--- /dev/null
+++ b/changelogs/unreleased/gitaly_feature_flag_metadata.yml
@@ -0,0 +1,5 @@
+---
+title: Add client and call site metadata to Gitaly calls for better traceability
+merge_request: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14332
+author:
+type: added
diff --git a/changelogs/unreleased/gitaly_ref_exists.yml b/changelogs/unreleased/gitaly_ref_exists.yml
deleted file mode 100644
index f62b646e406..00000000000
--- a/changelogs/unreleased/gitaly_ref_exists.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Implement the Gitaly RefService::RefExists endpoint
-merge_request: 13528
-author: Andrew Newdigate
diff --git a/changelogs/unreleased/group-milestones-breadcrumb.yml b/changelogs/unreleased/group-milestones-breadcrumb.yml
new file mode 100644
index 00000000000..87085759fda
--- /dev/null
+++ b/changelogs/unreleased/group-milestones-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone breadcrumb links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/group-mr-search-bar.yml b/changelogs/unreleased/group-mr-search-bar.yml
deleted file mode 100644
index 0b554a5d7c9..00000000000
--- a/changelogs/unreleased/group-mr-search-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add filtered search to group merge requests dashboard
-merge_request: 13688
-author: Hiroyuki Sato
-type: changed
diff --git a/changelogs/unreleased/group-sort-dropdown-blank.yml b/changelogs/unreleased/group-sort-dropdown-blank.yml
new file mode 100644
index 00000000000..dd16892be4d
--- /dev/null
+++ b/changelogs/unreleased/group-sort-dropdown-blank.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed group sort dropdown defaulting to empty
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/hash-mr-scroll-load.yml b/changelogs/unreleased/hash-mr-scroll-load.yml
new file mode 100644
index 00000000000..7e3965add03
--- /dev/null
+++ b/changelogs/unreleased/hash-mr-scroll-load.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed notes not being scrolled to in merge requests
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/hashed-storage-migration-path.yml b/changelogs/unreleased/hashed-storage-migration-path.yml
new file mode 100644
index 00000000000..5890eb09c38
--- /dev/null
+++ b/changelogs/unreleased/hashed-storage-migration-path.yml
@@ -0,0 +1,5 @@
+---
+title: Script to migrate project's repositories to new Hashed Storage
+merge_request: 14067
+author:
+type: added
diff --git a/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml
new file mode 100644
index 00000000000..22ac9b9073f
--- /dev/null
+++ b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Hide read_registry scope when registry is disabled on instance
+merge_request: 13314
+author: Robin Bobbitt
diff --git a/changelogs/unreleased/ie-event-polyfill.yml b/changelogs/unreleased/ie-event-polyfill.yml
new file mode 100644
index 00000000000..eaab089a47e
--- /dev/null
+++ b/changelogs/unreleased/ie-event-polyfill.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Event polyfill for IE11
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/import-sources-fix.yml b/changelogs/unreleased/import-sources-fix.yml
new file mode 100644
index 00000000000..03e23bc617c
--- /dev/null
+++ b/changelogs/unreleased/import-sources-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Read import sources from setting at first initialization
+merge_request: 14141
+author: Visay Keo
+type: fixed
diff --git a/changelogs/unreleased/improve-autocomplete-user-performance.yml b/changelogs/unreleased/improve-autocomplete-user-performance.yml
deleted file mode 100644
index 5a7153771ff..00000000000
--- a/changelogs/unreleased/improve-autocomplete-user-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance for AutocompleteController#users.json
-merge_request: 13754
-author: Hiroyuki Sato
-type: changed
diff --git a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml
deleted file mode 100644
index ec0d1b245e4..00000000000
--- a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: '"Share with group lock" now applies to subgroups, but owner can override setting
- on subgroups'
-merge_request: 13944
-author:
-type: changed
diff --git a/changelogs/unreleased/improve_sorting_list.yml b/changelogs/unreleased/improve_sorting_list.yml
new file mode 100644
index 00000000000..a3730e23ed1
--- /dev/null
+++ b/changelogs/unreleased/improve_sorting_list.yml
@@ -0,0 +1,5 @@
+---
+title: Improve list of sorting options
+merge_request: 14320
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/issue-api-my-reaction.yml b/changelogs/unreleased/issue-api-my-reaction.yml
deleted file mode 100644
index 1c12478fbc0..00000000000
--- a/changelogs/unreleased/issue-api-my-reaction.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add my_reaction_emoji param to /issues and /merge_requests API
-merge_request: 14016
-author: Hiroyuki Sato
-type: added
diff --git a/changelogs/unreleased/issue-boards-breadcrumbs-container.yml b/changelogs/unreleased/issue-boards-breadcrumbs-container.yml
deleted file mode 100644
index 5e042de7000..00000000000
--- a/changelogs/unreleased/issue-boards-breadcrumbs-container.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix breadcrumbs container in issue boards
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/issue_32215.yml b/changelogs/unreleased/issue_32215.yml
new file mode 100644
index 00000000000..c608eb6dd28
--- /dev/null
+++ b/changelogs/unreleased/issue_32215.yml
@@ -0,0 +1,5 @@
+---
+title: Allow developer role to admin milestones
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_35873.yml b/changelogs/unreleased/issue_35873.yml
new file mode 100644
index 00000000000..65064b97e56
--- /dev/null
+++ b/changelogs/unreleased/issue_35873.yml
@@ -0,0 +1,5 @@
+---
+title: Commenting on image diffs
+merge_request: 14061
+author:
+type: added
diff --git a/changelogs/unreleased/italicized_emoji.yml b/changelogs/unreleased/italicized_emoji.yml
new file mode 100644
index 00000000000..d3f15f94363
--- /dev/null
+++ b/changelogs/unreleased/italicized_emoji.yml
@@ -0,0 +1,5 @@
+---
+title: Update native unicode emojis to always render as normal text (previously could render italicized)
+merge_request:
+author: Branka Martinovic
+type: fixed
diff --git a/changelogs/unreleased/jobs-sort-by-id.yml b/changelogs/unreleased/jobs-sort-by-id.yml
new file mode 100644
index 00000000000..ec2c3a17b74
--- /dev/null
+++ b/changelogs/unreleased/jobs-sort-by-id.yml
@@ -0,0 +1,5 @@
+---
+title: Sort JobsController by id, not created_at
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/lint-changelog-yaml.yml b/changelogs/unreleased/lint-changelog-yaml.yml
new file mode 100644
index 00000000000..dcc8bf54827
--- /dev/null
+++ b/changelogs/unreleased/lint-changelog-yaml.yml
@@ -0,0 +1,5 @@
+---
+title: Detect when changelog entries are invalid
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/mentions-in-comments.yml b/changelogs/unreleased/mentions-in-comments.yml
new file mode 100644
index 00000000000..907f455007b
--- /dev/null
+++ b/changelogs/unreleased/mentions-in-comments.yml
@@ -0,0 +1,5 @@
+---
+title: Makes @mentions links have a different styling for better separation
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/merge-request-notes-performance.yml b/changelogs/unreleased/merge-request-notes-performance.yml
new file mode 100644
index 00000000000..6cf7a5047df
--- /dev/null
+++ b/changelogs/unreleased/merge-request-notes-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Use a UNION ALL for getting merge request notes
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/milestone-avatar-issuable-link.yml b/changelogs/unreleased/milestone-avatar-issuable-link.yml
new file mode 100644
index 00000000000..7915ad60fa8
--- /dev/null
+++ b/changelogs/unreleased/milestone-avatar-issuable-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone issuable assignee link URL
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mk-clarify-moving-namespaces.yml b/changelogs/unreleased/mk-clarify-moving-namespaces.yml
new file mode 100644
index 00000000000..8d89c296f61
--- /dev/null
+++ b/changelogs/unreleased/mk-clarify-moving-namespaces.yml
@@ -0,0 +1,5 @@
+---
+title: Expand docs for changing username or group path
+merge_request: 13914
+author:
+type: other
diff --git a/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml b/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml
deleted file mode 100644
index 865b57fb284..00000000000
--- a/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Default LDAP config "verify_certificates" to true for security
-merge_request: 13915
-author:
-type: changed
diff --git a/changelogs/unreleased/mk-normalize-ldap-user-dns.yml b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
new file mode 100644
index 00000000000..5a128d6acc1
--- /dev/null
+++ b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
@@ -0,0 +1,5 @@
+---
+title: Search or compare LDAP DNs case-insensitively and ignore excess whitespace
+merge_request: 14697
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-action.yml b/changelogs/unreleased/move-action.yml
deleted file mode 100644
index 65eceae3ef9..00000000000
--- a/changelogs/unreleased/move-action.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow users to move issues to other projects using a / command
-merge_request: 13436
-author: Manolis Mavrofidis
diff --git a/changelogs/unreleased/mr-index-page-performance.yml b/changelogs/unreleased/mr-index-page-performance.yml
deleted file mode 100644
index df5f44c04fa..00000000000
--- a/changelogs/unreleased/mr-index-page-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Re-use issue/MR counts for the pagination system
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml b/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml
new file mode 100644
index 00000000000..39b636bdfda
--- /dev/null
+++ b/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed breadcrumbs container expanding in side-by-side diff view
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mr-widget-merged-date-tooltip.yml b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
new file mode 100644
index 00000000000..ea22993ff52
--- /dev/null
+++ b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed merge request widget merged & closed date tooltip text
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/perf-slow-issuable.yml b/changelogs/unreleased/perf-slow-issuable.yml
deleted file mode 100644
index 29d15be1401..00000000000
--- a/changelogs/unreleased/perf-slow-issuable.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix repository equality check and avoid fetching ref if the commit is already
- available. This affects merge request creation performance
-merge_request: 13685
-author:
-type: other
diff --git a/changelogs/unreleased/project-page-clearer.yml b/changelogs/unreleased/project-page-clearer.yml
new file mode 100644
index 00000000000..7db01373360
--- /dev/null
+++ b/changelogs/unreleased/project-page-clearer.yml
@@ -0,0 +1,5 @@
+---
+title: Added tabs to dashboard/projects to easily switch to personal projects
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml b/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
new file mode 100644
index 00000000000..af359ce96b4
--- /dev/null
+++ b/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve GitHub import performance
+merge_request: 14445
+author:
+type: other
diff --git a/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml b/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml
new file mode 100644
index 00000000000..69695e403a9
--- /dev/null
+++ b/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml
@@ -0,0 +1,5 @@
+---
+title: Fix case sensitive email confirmation on signup
+merge_request: 14606
+author: robdel12
+type: fixed
diff --git a/changelogs/unreleased/refactor-animate-js.yml b/changelogs/unreleased/refactor-animate-js.yml
new file mode 100644
index 00000000000..ec32d68bbdd
--- /dev/null
+++ b/changelogs/unreleased/refactor-animate-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove animate.js and label animation.
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/refactor-monitoring-service.yml b/changelogs/unreleased/refactor-monitoring-service.yml
new file mode 100644
index 00000000000..685397cadb8
--- /dev/null
+++ b/changelogs/unreleased/refactor-monitoring-service.yml
@@ -0,0 +1,5 @@
+---
+title: Perform prometheus data endpoint requests in parallel
+merge_request: 14003
+author:
+type: fixed
diff --git a/changelogs/unreleased/remote_user.yml b/changelogs/unreleased/remote_user.yml
new file mode 100644
index 00000000000..75a941fa95f
--- /dev/null
+++ b/changelogs/unreleased/remote_user.yml
@@ -0,0 +1,4 @@
+---
+title: Add username as GL_USERNAME in hooks
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-temporary-ci-index.yml b/changelogs/unreleased/remove-temporary-ci-index.yml
new file mode 100644
index 00000000000..a319f7fff7f
--- /dev/null
+++ b/changelogs/unreleased/remove-temporary-ci-index.yml
@@ -0,0 +1,5 @@
+---
+title: Remove an index on ci_builds meant to be only temporary
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/remove-use-key-worker.yml b/changelogs/unreleased/remove-use-key-worker.yml
new file mode 100644
index 00000000000..a39bcae66bc
--- /dev/null
+++ b/changelogs/unreleased/remove-use-key-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Stop using Sidekiq for updating Key#last_used_at
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/remove_repo_prefix_from_api.yml b/changelogs/unreleased/remove_repo_prefix_from_api.yml
new file mode 100644
index 00000000000..bf2075e529c
--- /dev/null
+++ b/changelogs/unreleased/remove_repo_prefix_from_api.yml
@@ -0,0 +1,5 @@
+---
+title: Remove 'Repo' prefix from API entites
+merge_request: 14694
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_emails-feature.yml b/changelogs/unreleased/replace_emails-feature.yml
new file mode 100644
index 00000000000..d7f1a7a7ba9
--- /dev/null
+++ b/changelogs/unreleased/replace_emails-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the profile/emails.feature spinach test with an rspec analog
+merge_request: 14172
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_group_links-feature.yml b/changelogs/unreleased/replace_group_links-feature.yml
new file mode 100644
index 00000000000..7dd157632c9
--- /dev/null
+++ b/changelogs/unreleased/replace_group_links-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace project/group_links.feature spinach test with an rspec analog
+merge_request: 14169
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_milestone-feature.yml b/changelogs/unreleased/replace_milestone-feature.yml
new file mode 100644
index 00000000000..effe6d65645
--- /dev/null
+++ b/changelogs/unreleased/replace_milestone-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the project/milestone.feature spinach test with an rspec analog
+merge_request: 14171
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_profile_active_tab-feature.yml b/changelogs/unreleased/replace_profile_active_tab-feature.yml
new file mode 100644
index 00000000000..e911396a2b9
--- /dev/null
+++ b/changelogs/unreleased/replace_profile_active_tab-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'profile/active_tab.feature' spinach test with an rspec analog
+merge_request: 14239
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_archived-feature.yml b/changelogs/unreleased/replace_project_archived-feature.yml
new file mode 100644
index 00000000000..d0697347aa0
--- /dev/null
+++ b/changelogs/unreleased/replace_project_archived-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/archived.feature' spinach test with an rspec analog
+merge_request: 14322
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_builds_summary-feature.yml b/changelogs/unreleased/replace_project_builds_summary-feature.yml
new file mode 100644
index 00000000000..48652b39b7e
--- /dev/null
+++ b/changelogs/unreleased/replace_project_builds_summary-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/builds/summary.feature' spinach test with an rspec analog
+merge_request: 14177
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_commits_revert-feature.yml b/changelogs/unreleased/replace_project_commits_revert-feature.yml
new file mode 100644
index 00000000000..7fc9fcf3580
--- /dev/null
+++ b/changelogs/unreleased/replace_project_commits_revert-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/commits/revert.feature' spinach test with an rspec analog
+merge_request: 14325
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml
new file mode 100644
index 00000000000..a4a7435d4fa
--- /dev/null
+++ b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog
+merge_request: 14202
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests-feature.yml b/changelogs/unreleased/replace_project_merge_requests-feature.yml
new file mode 100644
index 00000000000..082c922a32b
--- /dev/null
+++ b/changelogs/unreleased/replace_project_merge_requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/merge_requests.feature' spinach test with an rspec analog
+merge_request: 14621
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml b/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml
new file mode 100644
index 00000000000..03562d6025e
--- /dev/null
+++ b/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/merge_requests/accept.feature' spinach test with an rspec analog
+merge_request: 14176
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml
new file mode 100644
index 00000000000..7d1ab4566b6
--- /dev/null
+++ b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml
@@ -0,0 +1,6 @@
+---
+title: Replace the 'project/merge_requests/revert.feature' spinach test with an rspec
+ analog
+merge_request: 14201
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_service-feature.yml b/changelogs/unreleased/replace_project_service-feature.yml
new file mode 100644
index 00000000000..11814732a9b
--- /dev/null
+++ b/changelogs/unreleased/replace_project_service-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/service.feature' spinach test with an rspec analog
+merge_request: 14432
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_shortcuts-feature.yml b/changelogs/unreleased/replace_project_shortcuts-feature.yml
new file mode 100644
index 00000000000..89e47a7a983
--- /dev/null
+++ b/changelogs/unreleased/replace_project_shortcuts-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/shortcuts.feature' spinach test with an rspec analog
+merge_request: 14431
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_snippets-feature.yml b/changelogs/unreleased/replace_project_snippets-feature.yml
new file mode 100644
index 00000000000..4fdee70008a
--- /dev/null
+++ b/changelogs/unreleased/replace_project_snippets-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/snippets.feature' spinach test with an rspec analog
+merge_request: 14326
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_search-feature.yml b/changelogs/unreleased/replace_search-feature.yml
new file mode 100644
index 00000000000..487f602ba30
--- /dev/null
+++ b/changelogs/unreleased/replace_search-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'search.feature' spinach test with an rspec analog
+merge_request: 14248
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_spinach_search_code-feature.yml b/changelogs/unreleased/replace_spinach_search_code-feature.yml
deleted file mode 100644
index 28d2108c871..00000000000
--- a/changelogs/unreleased/replace_spinach_search_code-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace 'source/search_code.feature' spinach test with an rspec analog
-merge_request: 13697
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/replace_spinach_star-feature.yml b/changelogs/unreleased/replace_spinach_star-feature.yml
deleted file mode 100644
index 6a058691fe5..00000000000
--- a/changelogs/unreleased/replace_spinach_star-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace 'project/star.feature' spinach test with an rspec analog
-merge_request: 13855
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_spinach_user_lookup-feature.yml b/changelogs/unreleased/replace_spinach_user_lookup-feature.yml
deleted file mode 100644
index 36248c54d99..00000000000
--- a/changelogs/unreleased/replace_spinach_user_lookup-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace 'project/user_lookup.feature' spinach test with an rspec analog
-merge_request: 13863
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_spinach_wiki-feature.yml b/changelogs/unreleased/replace_spinach_wiki-feature.yml
new file mode 100644
index 00000000000..a1801f1b58d
--- /dev/null
+++ b/changelogs/unreleased/replace_spinach_wiki-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace 'project/wiki.feature' spinach test with an rspec analog
+merge_request: 13856
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_team_management-feature.yml b/changelogs/unreleased/replace_team_management-feature.yml
new file mode 100644
index 00000000000..bc2bb17faf1
--- /dev/null
+++ b/changelogs/unreleased/replace_team_management-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the project/team_management.feature spinach test with an rspec analog
+merge_request: 14173
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/repository-name-emojis b/changelogs/unreleased/repository-name-emojis
deleted file mode 100644
index fe52df8eedc..00000000000
--- a/changelogs/unreleased/repository-name-emojis
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added ability to put emojis into repository name
-merge_request: 7420
-author: Vincent Composieux
diff --git a/changelogs/unreleased/rotated_profile_image.yml b/changelogs/unreleased/rotated_profile_image.yml
new file mode 100644
index 00000000000..1e221e47379
--- /dev/null
+++ b/changelogs/unreleased/rotated_profile_image.yml
@@ -0,0 +1,5 @@
+---
+title: Fix profile image orientation based on EXIF data gvieira37
+merge_request: 14461
+author: gvieira37
+type: fixed
diff --git a/changelogs/unreleased/rouge-2-2-0.yml b/changelogs/unreleased/rouge-2-2-0.yml
deleted file mode 100644
index 0b53cd14628..00000000000
--- a/changelogs/unreleased/rouge-2-2-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump rouge to v2.2.0
-merge_request: 13633
-author:
-type: other
diff --git a/changelogs/unreleased/rouge-2-2-1.yml b/changelogs/unreleased/rouge-2-2-1.yml
deleted file mode 100644
index 2d8879e5574..00000000000
--- a/changelogs/unreleased/rouge-2-2-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump rouge to v2.2.1
-merge_request: 13887
-author:
-type: other
diff --git a/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml b/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
new file mode 100644
index 00000000000..c9fb042aa37
--- /dev/null
+++ b/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Remove a SQL query from the todos index page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/seven-days-cycle-analytics.yml b/changelogs/unreleased/seven-days-cycle-analytics.yml
deleted file mode 100644
index ff660bdd603..00000000000
--- a/changelogs/unreleased/seven-days-cycle-analytics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a `Last 7 days` option for Cycle Analytics view
-merge_request: 13443
-author: Mehdi Lahmam (@mehlah)
-type: added
diff --git a/changelogs/unreleased/sh-add-grape-logging.yml b/changelogs/unreleased/sh-add-grape-logging.yml
deleted file mode 100644
index eaf6cb045d5..00000000000
--- a/changelogs/unreleased/sh-add-grape-logging.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add JSON logger in `log/api_json.log` for Grape API endpoints
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/sh-bump-jira-gem.yml b/changelogs/unreleased/sh-bump-jira-gem.yml
deleted file mode 100644
index d76b688caac..00000000000
--- a/changelogs/unreleased/sh-bump-jira-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-username-logging.yml b/changelogs/unreleased/sh-fix-username-logging.yml
new file mode 100644
index 00000000000..dadf3fb6729
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-username-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Fix username and ID not logging in production_json.log for Git activity
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-show-all-slack-names.yml b/changelogs/unreleased/sh-show-all-slack-names.yml
new file mode 100644
index 00000000000..f970cd0fb15
--- /dev/null
+++ b/changelogs/unreleased/sh-show-all-slack-names.yml
@@ -0,0 +1,5 @@
+---
+title: Include GitLab full name in Slack messages
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-thread-safe-markdown.yml b/changelogs/unreleased/sh-thread-safe-markdown.yml
new file mode 100644
index 00000000000..af7d9d58a9f
--- /dev/null
+++ b/changelogs/unreleased/sh-thread-safe-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Make Redcarpet Markdown renderer thread-safe
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sidebar-cache-updates.yml b/changelogs/unreleased/sidebar-cache-updates.yml
deleted file mode 100644
index aebe53ba5b2..00000000000
--- a/changelogs/unreleased/sidebar-cache-updates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only update the sidebar count caches when needed
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml b/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml
deleted file mode 100644
index 602ca358b8b..00000000000
--- a/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 'from commit' information to cherry-picked commits
-merge_request: 13475
-author: Saverio Miroddi
-type: added
diff --git a/changelogs/unreleased/tag-link-size.yml b/changelogs/unreleased/tag-link-size.yml
new file mode 100644
index 00000000000..d94e415ba1f
--- /dev/null
+++ b/changelogs/unreleased/tag-link-size.yml
@@ -0,0 +1,5 @@
+---
+title: Adjusts tag link to avoid underlining spaces
+merge_request: 14544
+author: Guilherme Vieira
+type: fixed
diff --git a/changelogs/unreleased/tc-geo-read-only-idea.yml b/changelogs/unreleased/tc-geo-read-only-idea.yml
new file mode 100644
index 00000000000..e1b52eef2ca
--- /dev/null
+++ b/changelogs/unreleased/tc-geo-read-only-idea.yml
@@ -0,0 +1,5 @@
+---
+title: Create idea of read-only database
+merge_request: 14688
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml b/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml
deleted file mode 100644
index 218336df5d2..00000000000
--- a/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migration to remove pending delete projects with non-existing namespace
-merge_request: 13598
-author:
-type: other
diff --git a/changelogs/unreleased/uipolish-fix-2factor-warning.yml b/changelogs/unreleased/uipolish-fix-2factor-warning.yml
new file mode 100644
index 00000000000..9f55207d309
--- /dev/null
+++ b/changelogs/unreleased/uipolish-fix-2factor-warning.yml
@@ -0,0 +1,5 @@
+---
+title: Two factor auth messages in settings no longer overlap the button
+merge_request:
+author: Jedidiah Broadbent
+type: fixed
diff --git a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
new file mode 100644
index 00000000000..34aa3d0db6f
--- /dev/null
+++ b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: Made the "remember me" check boxes have consistent styles and alignment
+merge_request:
+author: Jedidiah Broadbent
+type: fixed
diff --git a/changelogs/unreleased/update-pages-0-6.yml b/changelogs/unreleased/update-pages-0-6.yml
new file mode 100644
index 00000000000..507bb4d78e9
--- /dev/null
+++ b/changelogs/unreleased/update-pages-0-6.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Pages to v0.6.0
+merge_request: 14630
+author:
+type: other
diff --git a/changelogs/unreleased/url-sanitizer-fixes.yml b/changelogs/unreleased/url-sanitizer-fixes.yml
deleted file mode 100644
index 769036c829c..00000000000
--- a/changelogs/unreleased/url-sanitizer-fixes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix problems sanitizing URLs with empty passwords
-merge_request: 14083
-author:
-type: fixed
diff --git a/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml b/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml
deleted file mode 100644
index 0c3acce1455..00000000000
--- a/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use full path of project's avatar in webhooks
-merge_request: 13649
-author: Vitaliy @blackst0ne Klachkov
-type: changed
diff --git a/changelogs/unreleased/valid-branch-name-dash-bug.yml b/changelogs/unreleased/valid-branch-name-dash-bug.yml
new file mode 100644
index 00000000000..89e4578b3e5
--- /dev/null
+++ b/changelogs/unreleased/valid-branch-name-dash-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent branches or tags from starting with invalid characters (e.g. -, .)
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml b/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml
new file mode 100644
index 00000000000..faa5d3303c6
--- /dev/null
+++ b/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml
@@ -0,0 +1,5 @@
+---
+title: Removed two legacy config options
+merge_request:
+author: Daniel Voogsgerd
+type: deprecated
diff --git a/changelogs/unreleased/wiki_api.yml b/changelogs/unreleased/wiki_api.yml
deleted file mode 100644
index 9d60356aedc..00000000000
--- a/changelogs/unreleased/wiki_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API support for wiki pages
-merge_request: 13372
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/winh-delete-account-modal.yml b/changelogs/unreleased/winh-delete-account-modal.yml
new file mode 100644
index 00000000000..f1e2710fdcc
--- /dev/null
+++ b/changelogs/unreleased/winh-delete-account-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Show confirmation modal before deleting account
+merge_request: 14360
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-dropdown-changelog-docs.yml b/changelogs/unreleased/winh-dropdown-changelog-docs.yml
deleted file mode 100644
index 2f42b4dd9f9..00000000000
--- a/changelogs/unreleased/winh-dropdown-changelog-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Restyle dropdown menus to make them look consistent
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
new file mode 100644
index 00000000000..63f1f424a5d
--- /dev/null
+++ b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
@@ -0,0 +1,5 @@
+---
+title: Display whether branch has been merged when deleting protected branch
+merge_request: 14220
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-sprintf.yml b/changelogs/unreleased/winh-sprintf.yml
new file mode 100644
index 00000000000..f8ae5932ae4
--- /dev/null
+++ b/changelogs/unreleased/winh-sprintf.yml
@@ -0,0 +1,5 @@
+---
+title: Add basic sprintf implementation to JavaScript
+merge_request: 14506
+author:
+type: other
diff --git a/changelogs/unreleased/zj-add-pipeline-source-variable.yml b/changelogs/unreleased/zj-add-pipeline-source-variable.yml
deleted file mode 100644
index 5d98cd8086a..00000000000
--- a/changelogs/unreleased/zj-add-pipeline-source-variable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add CI_PIPELINE_SOURCE variable on CI Jobs
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/zj-auto-devops-table.yml b/changelogs/unreleased/zj-auto-devops-table.yml
deleted file mode 100644
index f1a004ebd19..00000000000
--- a/changelogs/unreleased/zj-auto-devops-table.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow users and administrator to configure Auto-DevOps
-merge_request: 13923
-author:
-type: added
diff --git a/changelogs/unreleased/zj-disable-pages-in-subgroups.yml b/changelogs/unreleased/zj-disable-pages-in-subgroups.yml
deleted file mode 100644
index 22c36214e1f..00000000000
--- a/changelogs/unreleased/zj-disable-pages-in-subgroups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove pages settings when not available
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/zj-remove-ci-api-v1.yml b/changelogs/unreleased/zj-remove-ci-api-v1.yml
deleted file mode 100644
index 8f2dc321b36..00000000000
--- a/changelogs/unreleased/zj-remove-ci-api-v1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove CI API v1
-merge_request:
-author:
-type: removed
diff --git a/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml b/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml
deleted file mode 100644
index 474392a8cdd..00000000000
--- a/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reword job to pipeline to reflect what the graphs are really about
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-sort-templates.yml b/changelogs/unreleased/zj-sort-templates.yml
deleted file mode 100644
index 443c4355890..00000000000
--- a/changelogs/unreleased/zj-sort-templates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Sort templates in the dropdown
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-update-rails-template.yml b/changelogs/unreleased/zj-update-rails-template.yml
deleted file mode 100644
index 5464f0e3d42..00000000000
--- a/changelogs/unreleased/zj-update-rails-template.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update Rails project template to use Postgresql by default
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml
deleted file mode 100644
index daa6a234c07..00000000000
--- a/changelogs/unreleased/zj-upgrade-grape.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade grape to 1.0
-merge_request:
-author:
-type: other
diff --git a/config/application.rb b/config/application.rb
index 32a290f2002..31e91835b9e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -51,7 +51,7 @@ module Gitlab
# Configure sensitive parameters which will be filtered from the log file.
#
# Parameters filtered:
- # - Any parameter ending with `_token`
+ # - Any parameter ending with `token`
# - Any parameter containing `password`
# - Any parameter containing `secret`
# - Two-factor tokens (:otp_attempt)
@@ -61,7 +61,7 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
- config.filter_parameters += [/_token$/, /password/, /secret/]
+ config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i(
certificate
encrypted_key
@@ -105,8 +105,7 @@ module Gitlab
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
- config.assets.precompile << "new_nav.css"
- config.assets.precompile << "new_sidebar.css"
+ config.assets.precompile << "locale/**/app.js"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
@@ -155,6 +154,9 @@ module Gitlab
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
ENV['GIT_TERMINAL_PROMPT'] = '0'
+ # Gitlab Read-only middleware support
+ config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
+
config.generators do |g|
g.factory_girl false
end
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index d6c3c84851b..db31b01a7d2 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -416,3 +416,51 @@
:why: https://gitlab.com/gitlab-com/organization/issues/117
:versions: []
:when: 2017-09-04 12:59:51.150798717 Z
+- - :approve
+ - console-browserify
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/console-browserify/blob/f0a8898487e2a47b8a5dc8734b91059fa2825506/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:13:07.073651000 Z
+- - :approve
+ - duplexer
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/duplexer/blob/master/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:14:15.774643000 Z
+- - :approve
+ - json3
+ - :who: Mike Greiling
+ :why: https://github.com/bestiejs/json3/blob/v3.3.2/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:15:16.273892000 Z
+- - :approve
+ - mime
+ - :who: Mike Greiling
+ :why: https://github.com/broofa/node-mime/blob/v1.3.4/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:16:21.135542000 Z
+- - :approve
+ - querystring-es3
+ - :who: Mike Greiling
+ :why: https://github.com/mike-spainhower/querystring/blob/v0.2.0/License.md
+ :versions: []
+ :when: 2017-09-16 05:17:20.372089000 Z
+- - :approve
+ - utils-merge
+ - :who: Mike Greiling
+ :why: https://github.com/jaredhanson/utils-merge/blob/v1.0.0/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:18:26.193764000 Z
+- - :approve
+ - svg4everybody
+ - :who: Tim Zallmann
+ :why: CC0 1.0 - https://github.com/jonathantneal/svg4everybody/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-09-13 17:31:16.425819400 Z
+- - :approve
+ - gitlab-svgs
+ - :who: Tim Zallmann
+ :why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs
+ :versions: []
+ :when: 2017-09-19 14:36:32.795496000 Z
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 278144b8943..1edb6fd39b8 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -16,7 +16,7 @@ Rails.application.configure do
config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
# Configure static asset server for tests with Cache-Control for performance
- config.assets.digest = false
+ config.assets.compile = false if ENV['CI']
config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index ec0d990aa0a..40ee85a3667 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -89,7 +89,7 @@ production: &base
# This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used.
# Tip: you can test your closing pattern at http://rubular.com.
- # 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+))+)'
+ # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)'
## Default project features settings
default_projects_features:
@@ -186,6 +186,7 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
+ artifacts_server: true
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
@@ -521,6 +522,8 @@ production: &base
# Gitaly settings
gitaly:
+ # Path to the directory containing Gitaly client executables.
+ client_path: /home/git/gitaly
# Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
@@ -599,12 +602,6 @@ production: &base
# Use the default values unless you really know what you are doing
git:
bin_path: /usr/bin/git
- # The next value is the maximum memory size grit can use
- # Given in number of bytes per git object (e.g. a commit)
- # This value can be increased if you have very large commits
- max_size: 20971520 # 20.megabytes
- # Git timeout to read a commit, in seconds
- timeout: 10
## Webpack settings
# If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running
@@ -714,7 +711,7 @@ test:
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly:
- enabled: true
+ client_path: tmp/tests/gitaly
token: secret
backup:
path: tmp/tests/backups
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e9893c0d4d6..ec80045696b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -257,7 +257,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].
Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil?
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['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
@@ -270,7 +270,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin
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['domain_whitelist'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea]
+Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
@@ -323,15 +323,16 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
-Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
-Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
-Settings.pages['https'] = false if Settings.pages['https'].nil?
-Settings.pages['host'] ||= "example.com"
-Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
-Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
-Settings.pages['url'] ||= Settings.__send__(:build_pages_url)
-Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
-Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+Settings.pages['host'] ||= "example.com"
+Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
+Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
+Settings.pages['url'] ||= Settings.__send__(:build_pages_url)
+Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
+Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
+Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pages['artifacts_server'].nil?
#
# Git LFS
@@ -513,9 +514,7 @@ Settings.backup['upload']['storage_class'] ||= nil
# Git
#
Settings['git'] ||= Settingslogic.new({})
-Settings.git['max_size'] ||= 20971520 # 20.megabytes
-Settings.git['bin_path'] ||= '/usr/bin/git'
-Settings.git['timeout'] ||= 10
+Settings.git['bin_path'] ||= '/usr/bin/git'
# Important: keep the satellites.path setting until GitLab 9.0 at
# least. This setting is fed to 'rm -rf' in
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 3aed2136f1b..c6ec0aeda7b 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -36,7 +36,7 @@ Devise.setup do |config|
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
- config.case_insensitive_keys = [:email]
+ config.case_insensitive_keys = [:email, :email_confirmation]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
@@ -175,7 +175,7 @@ Devise.setup do |config|
# Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user).
- # config.default_scope = :user
+ config.default_scope = :user # now have an :email scope as well, so set the default
# Configure sign_out behavior.
# Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope).
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 40e635bf2cf..b89f0419b91 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -58,7 +58,7 @@ Doorkeeper.configure do
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
- optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
+ optional_scopes(*Gitlab::Auth.optional_scopes)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
index c58f425b19b..af174def047 100644
--- a/config/initializers/doorkeeper_openid_connect.rb
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -1,7 +1,7 @@
Doorkeeper::OpenidConnect.configure do
issuer Gitlab.config.gitlab.url
- jws_private_key Rails.application.secrets.jws_private_key
+ signing_key Rails.application.secrets.openid_connect_signing_key
resource_owner_from_access_token do |access_token|
User.active.find_by(id: access_token.resource_owner_id)
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index 377e5104f9d..49551319435 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -39,3 +39,17 @@ module GettextI18nRailsJs
end
end
end
+
+class PoToJson
+ # This is required to modify the JS locale file output to our import needs
+ # Overwrites: https://github.com/webhippie/po_to_json/blob/master/lib/po_to_json.rb#L46
+ def generate_for_jed(language, overwrite = {})
+ @options = parse_options(overwrite.merge(language: language))
+ @parsed ||= inject_meta(parse_document)
+
+ generated = build_json_for(build_jed_for(@parsed))
+ [
+ "window.translations = #{generated};"
+ ].join(" ")
+ end
+end
diff --git a/config/initializers/grpc.rb b/config/initializers/grpc.rb
new file mode 100644
index 00000000000..b96962fe7db
--- /dev/null
+++ b/config/initializers/grpc.rb
@@ -0,0 +1,11 @@
+require 'logger'
+
+GRPC_LOGGER = Logger.new(Rails.root.join('log/grpc.log'))
+GRPC_LOGGER.level = ENV['GRPC_LOG_LEVEL'].presence || 'WARN'
+GRPC_LOGGER.progname = 'GRPC'
+
+module GRPC
+ def self.logger
+ GRPC_LOGGER
+ end
+end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 21fe8d72459..8560d24526f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -12,13 +12,18 @@ unless Sidekiq.server?
config.lograge.logger = ActiveSupport::Logger.new(filename)
# Add request parameters to log output
config.lograge.custom_options = lambda do |event|
- {
+ payload = {
time: event.time.utc.iso8601(3),
params: event.payload[:params].except(*%w(controller action format)),
remote_ip: event.payload[:remote_ip],
user_id: event.payload[:user_id],
username: event.payload[:username]
}
+
+ gitaly_calls = Gitlab::GitalyClient.get_request_count
+ payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0
+
+ payload
end
end
end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
index 820cc89ef57..c2f3023b330 100644
--- a/config/initializers/postgresql_opclasses_support.rb
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -127,7 +127,7 @@ module ActiveRecord
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
+ opclasses = Hash[inddef.scan(/\((.+?)\)(?:$| WHERE )/).flatten[0].split(',').map do |column_and_opclass|
column, opclass = column_and_opclass.split(' ').map(&:strip)
[column, opclass] if opclass
end.compact]
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index f9c1d2165d3..750a5b34f3b 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -25,7 +25,7 @@ def create_tokens
secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
db_key_base: generate_new_secure_token,
- jws_private_key: generate_new_rsa_private_key
+ openid_connect_signing_key: generate_new_rsa_private_key
}
missing_secrets = set_missing_keys(defaults)
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 62d0967009a..b2da3b3dc19 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -2,7 +2,7 @@
require 'gitlab/current_settings'
-if Rails.env.production?
+def configure_sentry
# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
sentry_enabled = Gitlab::CurrentSettings.current_application_settings.sentry_enabled
@@ -23,3 +23,5 @@ if Rails.env.production?
end
end
end
+
+configure_sentry if Rails.env.production?
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index 943e01f1496..d3a7a2b9f8b 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -30,7 +30,7 @@ if app.config.serve_static_files
settings.merge!(
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
- https: Gitlab.config.gitlab.https
+ https: false
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 0642a0b2fe9..33b897f46e2 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -4,12 +4,21 @@
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- - nginx_upstream_requests_total
+ - nginx_upstream_responses_total
weight: 1
queries:
- - query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
- label: Total
+ - query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)'
unit: req / sec
+ label: Status Code
+ series:
+ - label: status_code
+ when:
+ - value: 2xx
+ color: green
+ - value: 4xx
+ color: orange
+ - value: 5xx
+ color: red
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
@@ -37,9 +46,17 @@
- haproxy_frontend_http_requests_total
weight: 1
queries:
- - query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m]))'
- label: Total
+ - query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)'
unit: req / sec
+ series:
+ - label: code
+ when:
+ - value: 2xx
+ color: green
+ - value: 4xx
+ color: yellow
+ - value: 5xx
+ color: red
- title: "HTTP Error Rate"
y_label: "Error Rate (%)"
required_metrics:
@@ -86,12 +103,21 @@
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- - nginx_requests_total
+ - nginx_responses_total
weight: 1
queries:
- - query_range: 'sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))'
- label: Total
+ - query_range: 'sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code)'
unit: req / sec
+ label: Status Code
+ series:
+ - label: status_code
+ when:
+ - value: 2xx
+ color: green
+ - value: 4xx
+ color: orange
+ - value: 5xx
+ color: red
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
@@ -128,6 +154,8 @@
- container_cpu_usage_seconds_total
weight: 1
queries:
- - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}) * 100'
- label: Average
+ - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100'
+ label: CPU
unit: "%"
+ series:
+ - label: cpu
diff --git a/config/routes.rb b/config/routes.rb
index 5683725c8a2..405bfcc2d8e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -87,6 +87,7 @@ Rails.application.routes.draw do
resources :issues, module: :boards, only: [:index, :update]
end
+ draw :google_api
draw :import
draw :uploads
draw :explore
diff --git a/config/routes/google_api.rb b/config/routes/google_api.rb
new file mode 100644
index 00000000000..3fb236d3d51
--- /dev/null
+++ b/config/routes/google_api.rb
@@ -0,0 +1,5 @@
+namespace :google_api do
+ resource :auth, only: [], controller: :authorizations do
+ match :callback, via: [:get, :post]
+ end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 3e4e6111ab8..ddc852f0132 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -1,3 +1,6 @@
+# for secondary email confirmations - uses the same confirmation controller as :users
+devise_for :emails, path: 'profile/emails', controllers: { confirmations: :confirmations }
+
resource :profile, only: [:show, :update] do
member do
get :audit_log
@@ -28,7 +31,11 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
- resources :emails, only: [:index, :create, :destroy]
+ resources :emails, only: [:index, :create, :destroy] do
+ member do
+ put :resend_confirmation_instructions
+ end
+ end
resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do
delete :deny
diff --git a/config/routes/project.rb b/config/routes/project.rb
index b36d13888cd..7f0e056c884 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -183,6 +183,16 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resources :clusters, except: [:edit] do
+ collection do
+ get :login
+ end
+
+ member do
+ get :status, format: :json
+ end
+ end
+
resources :environments, except: [:destroy] do
member do
post :stop
@@ -271,7 +281,7 @@ constraints(ProjectUrlConstrainer.new) do
namespace :registry do
resources :repository, only: [] do
- resources :tags, only: [:destroy],
+ resources :tags, only: [:index, :destroy],
constraints: { id: Gitlab::Regex.container_registry_tag_regex }
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 883ffdcba4b..41b78bad8cc 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -38,7 +38,6 @@
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [upload_checksum, 1]
- - [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
@@ -64,3 +63,6 @@
- [propagate_service_template, 1]
- [background_migration, 1]
- [object_storage_upload, 1]
+ - [gcp_cluster, 1]
+ - [project_migrate_hashed_storage, 1]
+ - [storage_migrator, 1]
diff --git a/config/svg.config.js b/config/svg.config.js
new file mode 100644
index 00000000000..be72741abec
--- /dev/null
+++ b/config/svg.config.js
@@ -0,0 +1,48 @@
+/* eslint-disable no-commonjs */
+const path = require('path');
+const fs = require('fs');
+
+const sourcePath = path.join('node_modules', 'gitlab-svgs', 'dist');
+const sourcePathIllustrations = path.join('node_modules', 'gitlab-svgs', 'dist', 'illustrations');
+const destPath = path.normalize(path.join('app', 'assets', 'images'));
+
+// Actual Task copying the 2 files + all illustrations
+copyFileSync(path.join(sourcePath, 'icons.svg'), destPath);
+copyFileSync(path.join(sourcePath, 'icons.json'), destPath);
+copyFolderRecursiveSync(sourcePathIllustrations, destPath);
+
+// Helper Functions
+function copyFileSync(source, target) {
+ var targetFile = target;
+ //if target is a directory a new file with the same name will be created
+ if (fs.existsSync(target)) {
+ if (fs.lstatSync(target).isDirectory()) {
+ targetFile = path.join(target, path.basename(source));
+ }
+ }
+ console.log(`Copy SVG File : ${targetFile}`);
+ fs.writeFileSync(targetFile, fs.readFileSync(source));
+}
+
+function copyFolderRecursiveSync(source, target) {
+ var files = [];
+
+ //check if folder needs to be created or integrated
+ var targetFolder = path.join(target, path.basename(source));
+ if (!fs.existsSync(targetFolder)) {
+ fs.mkdirSync(targetFolder);
+ }
+
+ //copy
+ if (fs.lstatSync(source).isDirectory()) {
+ files = fs.readdirSync(source);
+ files.forEach(function (file) {
+ var curSource = path.join(source, file);
+ if (fs.lstatSync(curSource).isDirectory()) {
+ copyFolderRecursiveSync(curSource, targetFolder);
+ } else {
+ copyFileSync(curSource, targetFolder);
+ }
+ });
+ }
+}
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 6b0cd023291..8cded750a66 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -26,6 +26,7 @@ var config = {
},
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
+ account: './profile/account/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
@@ -45,6 +46,7 @@ var config = {
group: './group.js',
groups: './groups/index.js',
groups_list: './groups_list.js',
+ help: './help/help.js',
how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
@@ -67,6 +69,7 @@ var config = {
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches',
protected_tags: './protected_tags',
+ registry_list: './registry/index.js',
repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
@@ -121,10 +124,6 @@ var config = {
}
},
{
- test: /locale\/\w+\/(.*)\.js$/,
- loader: 'exports-loader?locales',
- },
- {
test: /monaco-editor\/\w+\/vs\/loader\.js$/,
use: [
{ loader: 'exports-loader', options: 'l.global' },
@@ -199,6 +198,7 @@ var config = {
'pdf_viewer',
'pipelines',
'pipelines_details',
+ 'registry_list',
'repo',
'schedule_form',
'schedules_index',
@@ -221,7 +221,7 @@ var config = {
// create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({
- names: ['main', 'locale', 'common', 'webpack_runtime'],
+ names: ['main', 'common', 'webpack_runtime'],
}),
// enable scope hoisting
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 6553c5d457a..1f8f5cfc82b 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -4,9 +4,9 @@ Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
'https://gitlab.com/gitlab-org/gitlab-test.git',
- 'https://gitlab.com/gitlab-org/gitlab-ce.git',
- 'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git',
+ 'https://gitlab.com/gnuwget/wget2.git',
+ 'https://gitlab.com/Commit451/LabCoat.git',
'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git',
diff --git a/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb b/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb
new file mode 100644
index 00000000000..3dafdf0fde4
--- /dev/null
+++ b/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb
@@ -0,0 +1,17 @@
+# rubocop:disable all
+class AddMergeRequestRebaseEnabledToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects, :merge_requests_rebase_enabled, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:projects, :merge_requests_rebase_enabled)
+ end
+end
diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
new file mode 100644
index 00000000000..6f22641077d
--- /dev/null
+++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
@@ -0,0 +1,19 @@
+# rubocop:disable all
+class AddFastForwardOptionToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ end
+
+ def down
+ if column_exists?(:projects, :merge_requests_ff_only_enabled)
+ remove_column(:projects, :merge_requests_ff_only_enabled)
+ end
+ end
+end
diff --git a/db/migrate/20160713200638_add_repository_read_only_to_projects.rb b/db/migrate/20160713200638_add_repository_read_only_to_projects.rb
new file mode 100644
index 00000000000..8ee8b55f210
--- /dev/null
+++ b/db/migrate/20160713200638_add_repository_read_only_to_projects.rb
@@ -0,0 +1,9 @@
+class AddRepositoryReadOnlyToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :repository_read_only, :boolean
+ end
+end
diff --git a/db/migrate/20170720122741_create_user_custom_attributes.rb b/db/migrate/20170720122741_create_user_custom_attributes.rb
new file mode 100644
index 00000000000..b1c0bebc633
--- /dev/null
+++ b/db/migrate/20170720122741_create_user_custom_attributes.rb
@@ -0,0 +1,17 @@
+class CreateUserCustomAttributes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :user_custom_attributes do |t|
+ t.timestamps_with_timezone null: false
+ t.references :user, null: false, foreign_key: { on_delete: :cascade }
+ t.string :key, null: false
+ t.string :value, null: false
+
+ t.index [:user_id, :key], unique: true
+ t.index [:key, :value]
+ end
+ end
+end
diff --git a/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb
new file mode 100644
index 00000000000..5bd777c53a0
--- /dev/null
+++ b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb
@@ -0,0 +1,13 @@
+class AddDiscussionLockedToIssuable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column(:merge_requests, :discussion_locked, :boolean)
+ add_column(:issues, :discussion_locked, :boolean)
+ end
+
+ def down
+ remove_column(:merge_requests, :discussion_locked)
+ remove_column(:issues, :discussion_locked)
+ end
+end
diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
index 592e141b7e6..f7ac87374b6 100644
--- a/db/migrate/20170828135939_migrate_user_external_mail_data.rb
+++ b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
@@ -33,7 +33,7 @@ class MigrateUserExternalMailData < ActiveRecord::Migration
SELECT true
FROM user_synced_attributes_metadata
WHERE user_id = users.id
- AND provider = users.email_provider
+ AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL))
)
AND id BETWEEN #{start_id} AND #{end_id}
EOF
diff --git a/db/migrate/20170830131015_swap_event_migration_tables.rb b/db/migrate/20170830131015_swap_event_migration_tables.rb
index 5128d1b2fe7..a256de4a8af 100644
--- a/db/migrate/20170830131015_swap_event_migration_tables.rb
+++ b/db/migrate/20170830131015_swap_event_migration_tables.rb
@@ -7,6 +7,10 @@ class SwapEventMigrationTables < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ class Event < ActiveRecord::Base
+ self.table_name = 'events'
+ end
+
def up
rename_tables
end
@@ -19,5 +23,25 @@ class SwapEventMigrationTables < ActiveRecord::Migration
rename_table :events, :events_old
rename_table :events_for_migration, :events
rename_table :events_old, :events_for_migration
+
+ # Once swapped we need to reset the primary key of the new "events" table to
+ # make sure that data created starts with the right value. This isn't
+ # necessary for events_for_migration since we replicate existing primary key
+ # values to it.
+ if Gitlab::Database.postgresql?
+ reset_primary_key_for_postgresql
+ else
+ reset_primary_key_for_mysql
+ end
+ end
+
+ def reset_primary_key_for_postgresql
+ reset_pk_sequence!(Event.table_name)
+ end
+
+ def reset_primary_key_for_mysql
+ amount = Event.pluck('COALESCE(MAX(id), 1)').first
+
+ execute "ALTER TABLE #{Event.table_name} AUTO_INCREMENT = #{amount}"
end
end
diff --git a/db/migrate/20170904092148_add_email_confirmation.rb b/db/migrate/20170904092148_add_email_confirmation.rb
new file mode 100644
index 00000000000..17ff424b319
--- /dev/null
+++ b/db/migrate/20170904092148_add_email_confirmation.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEmailConfirmation < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :emails, :confirmation_token, :string
+ add_column :emails, :confirmed_at, :datetime_with_timezone
+ add_column :emails, :confirmation_sent_at, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20170909090114_add_email_confirmation_index.rb b/db/migrate/20170909090114_add_email_confirmation_index.rb
new file mode 100644
index 00000000000..a8c1023c482
--- /dev/null
+++ b/db/migrate/20170909090114_add_email_confirmation_index.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEmailConfirmationIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ # Not necessary to remove duplicates, as :confirmation_token is a new column
+ def up
+ add_concurrent_index :emails, :confirmation_token, unique: true
+ end
+
+ def down
+ remove_concurrent_index :emails, :confirmation_token if index_exists?(:emails, :confirmation_token)
+ end
+end
diff --git a/db/migrate/20170912113435_clean_stages_statuses_migration.rb b/db/migrate/20170912113435_clean_stages_statuses_migration.rb
new file mode 100644
index 00000000000..fc091d7894e
--- /dev/null
+++ b/db/migrate/20170912113435_clean_stages_statuses_migration.rb
@@ -0,0 +1,26 @@
+class CleanStagesStatusesMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Stage < ActiveRecord::Base
+ include ::EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('MigrateStageStatus')
+
+ Stage.where('status IS NULL').each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::MigrateStageStatus.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20170913131410_environments_project_id_not_null.rb b/db/migrate/20170913131410_environments_project_id_not_null.rb
new file mode 100644
index 00000000000..d5404f8ede9
--- /dev/null
+++ b/db/migrate/20170913131410_environments_project_id_not_null.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class EnvironmentsProjectIdNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :environments, :project_id, false
+ end
+
+ def down
+ change_column_null :environments, :project_id, true
+ end
+end
diff --git a/db/migrate/20170914135630_add_index_for_recent_push_events.rb b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
new file mode 100644
index 00000000000..99f593b0465
--- /dev/null
+++ b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
@@ -0,0 +1,40 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexForRecentPushEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index_if_not_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+
+ remove_concurrent_index_if_present(:merge_requests, :source_project_id)
+ end
+
+ def down
+ add_concurrent_index_if_not_present(:merge_requests, :source_project_id)
+
+ remove_concurrent_index_if_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+ end
+
+ def add_concurrent_index_if_not_present(table, columns)
+ return if index_exists?(table, columns)
+
+ add_concurrent_index(table, columns)
+ end
+
+ def remove_concurrent_index_if_present(table, columns)
+ return unless index_exists?(table, columns)
+
+ remove_concurrent_index(table, columns)
+ end
+end
diff --git a/db/migrate/20170918222253_reorganize_deployments_indexes.rb b/db/migrate/20170918222253_reorganize_deployments_indexes.rb
new file mode 100644
index 00000000000..139427ed2b9
--- /dev/null
+++ b/db/migrate/20170918222253_reorganize_deployments_indexes.rb
@@ -0,0 +1,28 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ReorganizeDeploymentsIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_index_if_not_exists :deployments, [:environment_id, :iid, :project_id]
+ remove_index_if_exists :deployments, [:project_id, :environment_id, :iid]
+ end
+
+ def down
+ add_index_if_not_exists :deployments, [:project_id, :environment_id, :iid]
+ remove_index_if_exists :deployments, [:environment_id, :iid, :project_id]
+ end
+
+ def add_index_if_not_exists(table, columns)
+ add_concurrent_index(table, columns) unless index_exists?(table, columns)
+ end
+
+ def remove_index_if_exists(table, columns)
+ remove_concurrent_index(table, columns) if index_exists?(table, columns)
+ end
+end
diff --git a/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb
new file mode 100644
index 00000000000..b91efb86d98
--- /dev/null
+++ b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDeploymentsIndexForLastDeployment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TO_INDEX = [:deployments, %i[environment_id id]].freeze
+
+ def up
+ add_concurrent_index(*TO_INDEX)
+ end
+
+ def down
+ remove_concurrent_index(*TO_INDEX)
+ end
+end
diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
new file mode 100644
index 00000000000..b2009b282e9
--- /dev/null
+++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # To use create/remove index concurrently
+ disable_ddl_transaction!
+
+ def up
+ return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration')
+ remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration")
+ end
+
+ def down
+ # this was a temporary index for a migration that was never
+ # present previously so this probably shouldn't be here but it's
+ # easier to test the drop if we have a way to create it.
+ add_concurrent_index("ci_builds", ["id"],
+ name: "index_for_ci_builds_retried_migration",
+ where: "(retried IS NULL)",
+ using: :btree)
+ end
+end
diff --git a/db/migrate/20170921115009_add_project_repository_storage_index.rb b/db/migrate/20170921115009_add_project_repository_storage_index.rb
new file mode 100644
index 00000000000..1c5a8fd65e1
--- /dev/null
+++ b/db/migrate/20170921115009_add_project_repository_storage_index.rb
@@ -0,0 +1,19 @@
+class AddProjectRepositoryStorageIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(*index_spec) unless index_exists?(*index_spec)
+ end
+
+ def down
+ remove_concurrent_index(*index_spec) if index_exists?(*index_spec)
+ end
+
+ def index_spec
+ [:projects, :repository_storage]
+ end
+end
diff --git a/db/migrate/20170924094327_create_gcp_clusters.rb b/db/migrate/20170924094327_create_gcp_clusters.rb
new file mode 100644
index 00000000000..657dddcbbc4
--- /dev/null
+++ b/db/migrate/20170924094327_create_gcp_clusters.rb
@@ -0,0 +1,45 @@
+class CreateGcpClusters < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :gcp_clusters do |t|
+ # Order columns by best align scheme
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :user, foreign_key: { on_delete: :nullify }
+ t.references :service, foreign_key: { on_delete: :nullify }
+ t.integer :status
+ t.integer :gcp_cluster_size, null: false
+
+ # Timestamps
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ # Enable/disable
+ t.boolean :enabled, default: true
+
+ # General
+ t.text :status_reason
+
+ # k8s integration specific
+ t.string :project_namespace
+
+ # Cluster details
+ t.string :endpoint
+ t.text :ca_cert
+ t.text :encrypted_kubernetes_token
+ t.string :encrypted_kubernetes_token_iv
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ # GKE
+ t.string :gcp_project_id, null: false
+ t.string :gcp_cluster_zone, null: false
+ t.string :gcp_cluster_name, null: false
+ t.string :gcp_machine_type
+ t.string :gcp_operation_id
+ t.text :encrypted_gcp_token
+ t.string :encrypted_gcp_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb
new file mode 100644
index 00000000000..c2cb1df2586
--- /dev/null
+++ b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCiBuildsIndexForJobscontroller < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, [:project_id, :id] unless index_exists? :ci_builds, [:project_id, :id]
+ remove_concurrent_index :ci_builds, :project_id if index_exists? :ci_builds, :project_id
+ end
+
+ def down
+ add_concurrent_index :ci_builds, :project_id unless index_exists? :ci_builds, :project_id
+ remove_concurrent_index :ci_builds, [:project_id, :id] if index_exists? :ci_builds, [:project_id, :id]
+ end
+end
diff --git a/db/migrate/20170927122209_add_partial_index_for_labels_template.rb b/db/migrate/20170927122209_add_partial_index_for_labels_template.rb
new file mode 100644
index 00000000000..c3e5077ba20
--- /dev/null
+++ b/db/migrate/20170927122209_add_partial_index_for_labels_template.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPartialIndexForLabelsTemplate < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+
+ disable_ddl_transaction!
+
+ # Note this is a partial index in Postgres but MySQL will ignore the
+ # partial index clause. By making it an index on "template" this
+ # means the index will still accomplish the same goal of optimizing
+ # a query with "where template = true" on MySQL -- it'll just take
+ # more space. In this case the number of records with template=true
+ # is expected to be very small (small enough to display on a single
+ # web page) so it's ok to filter or sort them without the index
+ # anyways.
+
+ def up
+ add_concurrent_index "labels", ["template"], where: "template"
+ end
+
+ def down
+ remove_concurrent_index "labels", ["template"], where: "template"
+ end
+end
diff --git a/db/migrate/20170927161718_create_gpg_key_subkeys.rb b/db/migrate/20170927161718_create_gpg_key_subkeys.rb
new file mode 100644
index 00000000000..c03c40416a8
--- /dev/null
+++ b/db/migrate/20170927161718_create_gpg_key_subkeys.rb
@@ -0,0 +1,23 @@
+class CreateGpgKeySubkeys < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ create_table :gpg_key_subkeys do |t|
+ t.references :gpg_key, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.binary :keyid
+ t.binary :fingerprint
+
+ t.index :keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
+ t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
+ end
+
+ add_reference :gpg_signatures, :gpg_key_subkey, index: true, foreign_key: { on_delete: :nullify }
+ end
+
+ def down
+ remove_reference(:gpg_signatures, :gpg_key_subkey, index: true, foreign_key: true)
+
+ drop_table :gpg_key_subkeys
+ end
+end
diff --git a/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb b/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb
new file mode 100644
index 00000000000..9f02daf04c1
--- /dev/null
+++ b/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCompositeIndexOnMergeRequestsMergeCommitSha < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # The default index name is too long for PostgreSQL and would thus be
+ # truncated.
+ INDEX_NAME = 'index_merge_requests_on_tp_id_and_merge_commit_sha_and_id'
+
+ COLUMNS = [:target_project_id, :merge_commit_sha, :id]
+
+ disable_ddl_transaction!
+
+ def up
+ return if index_is_present?
+
+ add_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME)
+ end
+
+ def down
+ return unless index_is_present?
+
+ remove_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME)
+ end
+
+ def index_is_present?
+ index_exists?(:merge_requests, COLUMNS, name: INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20170928124105_create_fork_networks.rb b/db/migrate/20170928124105_create_fork_networks.rb
new file mode 100644
index 00000000000..ca906b953a3
--- /dev/null
+++ b/db/migrate/20170928124105_create_fork_networks.rb
@@ -0,0 +1,28 @@
+class CreateForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :fork_networks do |t|
+ t.references :root_project,
+ references: :projects,
+ index: { unique: true }
+
+ t.string :deleted_root_project_name
+ end
+
+ add_concurrent_foreign_key :fork_networks, :projects,
+ column: :root_project_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:fork_networks, :root_project_id).any?
+ remove_foreign_key :fork_networks, column: :root_project_id
+ end
+ drop_table :fork_networks
+ end
+end
diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb
new file mode 100644
index 00000000000..836f023efdc
--- /dev/null
+++ b/db/migrate/20170928133643_create_fork_network_members.rb
@@ -0,0 +1,26 @@
+class CreateForkNetworkMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :fork_network_members do |t|
+ t.references :fork_network, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :forked_from_project, references: :projects
+ end
+
+ add_concurrent_foreign_key :fork_network_members, :projects,
+ column: :forked_from_project_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:fork_network_members, :forked_from_project_id).any?
+ remove_foreign_key :fork_network_members, column: :forked_from_project_id
+ end
+ drop_table :fork_network_members
+ end
+end
diff --git a/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb b/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb
new file mode 100644
index 00000000000..82adddbc1ec
--- /dev/null
+++ b/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb
@@ -0,0 +1,9 @@
+class AddFailureReasonToPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_pipelines, :failure_reason, :integer
+ end
+end
diff --git a/db/migrate/20170929131201_populate_fork_networks.rb b/db/migrate/20170929131201_populate_fork_networks.rb
new file mode 100644
index 00000000000..1214962770f
--- /dev/null
+++ b/db/migrate/20170929131201_populate_fork_networks.rb
@@ -0,0 +1,30 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'PopulateForkNetworksRange'.freeze
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 15.seconds
+
+ disable_ddl_transaction!
+
+ class ForkedProjectLink < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'forked_project_links'
+ end
+
+ def up
+ say 'Populating the `fork_networks` based on existing `forked_project_links`'
+
+ queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb
new file mode 100644
index 00000000000..ac266c3e22e
--- /dev/null
+++ b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb
@@ -0,0 +1,25 @@
+# rubocop:disable all
+class MakeSureFastForwardOptionExists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # We had to fix the migration db/migrate/20150827121444_add_fast_forward_option_to_project.rb
+ # And this is why it's possible that someone has ran the migrations but does
+ # not have the merge_requests_ff_only_enabled column. This migration makes sure it will
+ # be added
+ unless column_exists?(:projects, :merge_requests_ff_only_enabled)
+ add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ end
+ end
+
+ def down
+ if column_exists?(:projects, :merge_requests_ff_only_enabled)
+ remove_column(:projects, :merge_requests_ff_only_enabled)
+ end
+ end
+end
diff --git a/db/migrate/20171006090001_create_ci_build_trace_sections.rb b/db/migrate/20171006090001_create_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..ab5ef319618
--- /dev/null
+++ b/db/migrate/20171006090001_create_ci_build_trace_sections.rb
@@ -0,0 +1,19 @@
+class CreateCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_build_trace_sections do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.datetime_with_timezone :date_start, null: false
+ t.datetime_with_timezone :date_end, null: false
+ t.integer :byte_start, limit: 8, null: false
+ t.integer :byte_end, limit: 8, null: false
+ t.integer :build_id, null: false
+ t.integer :section_name_id, null: false
+ end
+
+ add_index :ci_build_trace_sections, [:build_id, :section_name_id], unique: true
+ end
+end
diff --git a/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb b/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..d279463eb4b
--- /dev/null
+++ b/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb
@@ -0,0 +1,15 @@
+class AddBuildForeignKeyToCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ci_build_trace_sections, :ci_builds, column: :build_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_build_trace_sections, column: :build_id)
+ end
+end
diff --git a/db/migrate/20171006090100_create_ci_build_trace_section_names.rb b/db/migrate/20171006090100_create_ci_build_trace_section_names.rb
new file mode 100644
index 00000000000..88f3e60699a
--- /dev/null
+++ b/db/migrate/20171006090100_create_ci_build_trace_section_names.rb
@@ -0,0 +1,19 @@
+class CreateCiBuildTraceSectionNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :ci_build_trace_section_names do |t|
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.string :name, null: false
+ end
+
+ add_index :ci_build_trace_section_names, [:project_id, :name], unique: true
+ end
+
+ def down
+ remove_foreign_key :ci_build_trace_section_names, column: :project_id
+ drop_table :ci_build_trace_section_names
+ end
+end
diff --git a/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb b/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..08422885a98
--- /dev/null
+++ b/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb
@@ -0,0 +1,15 @@
+class AddNameForeignKeyToCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ci_build_trace_sections, :ci_build_trace_section_names, column: :section_name_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_build_trace_sections, column: :section_name_id)
+ end
+end
diff --git a/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb
index fefd931e5d2..fd1437b07f5 100644
--- a/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb
+++ b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb
@@ -33,7 +33,7 @@ class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration
SELECT true
FROM user_synced_attributes_metadata
WHERE user_id = users.id
- AND provider = users.email_provider
+ AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL))
)
AND id BETWEEN #{start_id} AND #{end_id}
EOF
diff --git a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
new file mode 100644
index 00000000000..3e84b295be4
--- /dev/null
+++ b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class DeleteConflictingRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze
+ BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s.
+ DELAY_INTERVAL = 12.seconds
+
+ disable_ddl_transaction!
+
+ class Route < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'routes'
+ end
+
+ def up
+ say opening_message
+
+ queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+
+ def opening_message
+ <<~MSG
+ Clean up redirect routes that conflict with regular routes.
+ See initial bug fix:
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357
+ MSG
+ end
+end
diff --git a/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
new file mode 100644
index 00000000000..bfa9ad80c7d
--- /dev/null
+++ b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
@@ -0,0 +1,33 @@
+class FixProjectsWithoutProjectFeature < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ # Deletes corrupted project features
+ sql = "DELETE FROM project_features WHERE project_id IS NULL"
+ execute(sql)
+
+ # Creates missing project features with private visibility
+ sql =
+ %Q{
+ INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level,
+ builds_access_level, snippets_access_level, created_at, updated_at)
+ SELECT projects.id as project_id,
+ 10 as repository_access_level,
+ 10 as issues_access_level,
+ 10 as merge_requests_access_level,
+ 10 as wiki_access_level,
+ 10 as builds_access_level ,
+ 10 as snippets_access_level,
+ projects.created_at,
+ projects.updated_at
+ FROM projects
+ LEFT OUTER JOIN project_features ON project_features.project_id = projects.id
+ WHERE (project_features.id IS NULL)
+ }
+
+ execute(sql)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb
new file mode 100644
index 00000000000..2230bb0e53c
--- /dev/null
+++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class NormalizeLdapExternUids < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'NormalizeLdapExternUidsRange'.freeze
+ DELAY_INTERVAL = 10.seconds
+
+ disable_ddl_transaction!
+
+ class Identity < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'identities'
+ end
+
+ def up
+ ldap_identities = Identity.where("provider like 'ldap%'")
+
+ if ldap_identities.any?
+ queue_background_migration_jobs_by_range_at_intervals(Identity, MIGRATION, DELAY_INTERVAL)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb b/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb
new file mode 100644
index 00000000000..5732cb85ea5
--- /dev/null
+++ b/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb
@@ -0,0 +1,32 @@
+class ScheduleMergeRequestDiffMigrationsTakeTwo < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 500
+ MIGRATION = 'DeserializeMergeRequestDiffsAndCommits'
+ DELAY_INTERVAL = 10.minutes
+
+ disable_ddl_transaction!
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+
+ default_scope { where('st_commits IS NOT NULL OR st_diffs IS NOT NULL') }
+ end
+
+ # By this point, we assume ScheduleMergeRequestDiffMigrations - the first
+ # version of this - has already run. On GitLab.com, we have ~220k un-migrated
+ # rows, but these rows will, in general, take a long time.
+ #
+ # With a gap of 10 minutes per batch, and 500 rows per batch, these migrations
+ # are scheduled over 220_000 / 500 / 6 ~= 74 hours, which is a little over
+ # three days.
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(MergeRequestDiff, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
new file mode 100644
index 00000000000..a238216253b
--- /dev/null
+++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
@@ -0,0 +1,16 @@
+class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:notes, :type, 'LegacyDiffNote') do |table, query|
+ query.where(table[:type].eq('Github::Import::LegacyDiffNote'))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
new file mode 100644
index 00000000000..1e70acd9868
--- /dev/null
+++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
@@ -0,0 +1,16 @@
+class UpdateNotesTypeForImport < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:notes, :type, 'Note') do |table, query|
+ query.where(table[:type].eq('Github::Import::Note'))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb b/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb
new file mode 100644
index 00000000000..01d56fbd490
--- /dev/null
+++ b/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb
@@ -0,0 +1,28 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ScheduleCreateGpgKeySubkeysFromGpgKeys < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ MIGRATION = 'CreateGpgKeySubkeysFromGpgKeys'
+
+ class GpgKey < ActiveRecord::Base
+ self.table_name = 'gpg_keys'
+
+ include EachBatch
+ end
+
+ def up
+ GpgKey.select(:id).each_batch do |gpg_keys|
+ jobs = gpg_keys.pluck(:id).map do |id|
+ [MIGRATION, [id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9f293205a24..8462581908b 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: 20170905112933) do
+ActiveRecord::Schema.define(version: 20171006091000) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -207,6 +207,26 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+ create_table "ci_build_trace_section_names", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.string "name", null: false
+ end
+
+ add_index "ci_build_trace_section_names", ["project_id", "name"], name: "index_ci_build_trace_section_names_on_project_id_and_name", unique: true, using: :btree
+
+ create_table "ci_build_trace_sections", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.datetime_with_timezone "date_start", null: false
+ t.datetime_with_timezone "date_end", null: false
+ t.integer "byte_start", limit: 8, null: false
+ t.integer "byte_end", limit: 8, null: false
+ t.integer "build_id", null: false
+ t.integer "section_name_id", null: false
+ end
+
+ add_index "ci_build_trace_sections", ["build_id", "section_name_id"], name: "index_ci_build_trace_sections_on_build_id_and_section_name_id", unique: true, using: :btree
+ add_index "ci_build_trace_sections", ["project_id"], name: "index_ci_build_trace_sections_on_project_id", using: :btree
+
create_table "ci_builds", force: :cascade do |t|
t.string "status"
t.datetime "finished_at"
@@ -258,8 +278,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"}
- add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
@@ -345,6 +364,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "source"
t.integer "config_source"
t.boolean "protected"
+ t.integer "failure_reason"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
@@ -508,7 +528,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
end
add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
- add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
+ add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
+ add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
create_table "emails", force: :cascade do |t|
@@ -516,13 +537,17 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.string "email", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "confirmation_token"
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
end
+ add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "environments", force: :cascade do |t|
- t.integer "project_id"
+ t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -568,6 +593,22 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree
+ create_table "fork_network_members", force: :cascade do |t|
+ t.integer "fork_network_id", null: false
+ t.integer "project_id", null: false
+ t.integer "forked_from_project_id"
+ end
+
+ add_index "fork_network_members", ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree
+ add_index "fork_network_members", ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
+
+ create_table "fork_networks", force: :cascade do |t|
+ t.integer "root_project_id"
+ t.string "deleted_root_project_name"
+ end
+
+ add_index "fork_networks", ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree
+
create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
@@ -577,6 +618,45 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
+ create_table "gcp_clusters", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "user_id"
+ t.integer "service_id"
+ t.integer "status"
+ t.integer "gcp_cluster_size", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.text "status_reason"
+ t.string "project_namespace"
+ t.string "endpoint"
+ t.text "ca_cert"
+ t.text "encrypted_kubernetes_token"
+ t.string "encrypted_kubernetes_token_iv"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.string "gcp_project_id", null: false
+ t.string "gcp_cluster_zone", null: false
+ t.string "gcp_cluster_name", null: false
+ t.string "gcp_machine_type"
+ t.string "gcp_operation_id"
+ t.text "encrypted_gcp_token"
+ t.string "encrypted_gcp_token_iv"
+ end
+
+ add_index "gcp_clusters", ["project_id"], name: "index_gcp_clusters_on_project_id", unique: true, using: :btree
+
+ create_table "gpg_key_subkeys", force: :cascade do |t|
+ t.integer "gpg_key_id", null: false
+ t.binary "keyid"
+ t.binary "fingerprint"
+ end
+
+ add_index "gpg_key_subkeys", ["fingerprint"], name: "index_gpg_key_subkeys_on_fingerprint", unique: true, using: :btree
+ add_index "gpg_key_subkeys", ["gpg_key_id"], name: "index_gpg_key_subkeys_on_gpg_key_id", using: :btree
+ add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
+
create_table "gpg_keys", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -600,11 +680,13 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.text "gpg_key_user_name"
t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false
+ t.integer "gpg_key_subkey_id"
end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
+ add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
create_table "identities", force: :cascade do |t|
@@ -662,6 +744,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
+ t.boolean "discussion_locked"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -732,6 +815,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+ add_index "labels", ["template"], name: "index_labels_on_template", where: "template", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
@@ -885,6 +969,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "head_pipeline_id"
t.boolean "ref_fetched"
t.string "merge_jid"
+ t.boolean "discussion_locked"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -895,9 +980,10 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
+ add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
+ add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -1218,6 +1304,9 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.datetime "last_repository_updated_at"
t.integer "storage_version", limit: 2
t.boolean "resolve_outdated_diff_discussions"
+ t.boolean "repository_read_only"
+ t.boolean "merge_requests_ff_only_enabled", default: false
+ t.boolean "merge_requests_rebase_enabled", default: false, null: false
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1232,6 +1321,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
+ add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
@@ -1536,6 +1626,17 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
+ create_table "user_custom_attributes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "user_id", null: false
+ t.string "key", null: false
+ t.string "value", null: false
+ end
+
+ add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree
+ add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree
+
create_table "user_synced_attributes_metadata", force: :cascade do |t|
t.boolean "name_synced", default: false
t.boolean "email_synced", default: false
@@ -1683,6 +1784,10 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+ add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "ci_build_trace_section_names", column: "section_name_id", name: "fk_264e112c66", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "ci_builds", column: "build_id", name: "fk_4ebe41f502", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "projects", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
@@ -1707,8 +1812,17 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
+ add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
+ add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
+ add_foreign_key "fork_network_members", "projects", on_delete: :cascade
+ add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
+ add_foreign_key "gcp_clusters", "projects", on_delete: :cascade
+ add_foreign_key "gcp_clusters", "services", on_delete: :nullify
+ add_foreign_key "gcp_clusters", "users", on_delete: :nullify
+ add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
+ add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
@@ -1762,6 +1876,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
+ add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index b3e7c9bd0bf..c6500a37aa9 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -53,6 +53,7 @@ Shortcuts to GitLab's most visited docs:
### Projects and groups
- [Projects](user/project/index.md):
+ - [Project settings](user/project/settings/index.md)
- [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).
@@ -68,17 +69,18 @@ Shortcuts to GitLab's most visited docs:
Manage your [repositories](user/project/repository/index.md) from the UI (user interface):
-- Files
+- [Files](user/project/repository/index.md#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
+- [Branches](user/project/repository/branches/index.md)
+ - [Default branch](user/project/repository/branches/index.md#default-branch)
- [Create a branch](user/project/repository/web_editor.md#create-a-new-branch)
- [Protected branches](user/project/protected_branches.md#protected-branches)
- [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches)
-- Commits
+- [Commits](user/project/repository/index.md#commits)
- [Signing commits](user/project/repository/gpg_signed_commits/index.md): use GPG to sign your commits.
### Issues and Merge Requests (MRs)
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index d22815dfa5e..ad903aef896 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -256,7 +256,7 @@ production:
```
Tip: If you want to limit access to the nested members of an Active Directory
-group you can use the following syntax:
+group, you can use the following syntax:
```
(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
@@ -287,11 +287,11 @@ LDAP email address, and then sign into GitLab via their LDAP credentials.
There are two encryption methods, `simple_tls` and `start_tls`.
-For either encryption method, if setting `validate_certificates: false`, TLS
+For either encryption method, if setting `verify_certificates: false`, TLS
encryption is established with the LDAP server before any LDAP-protocol data is
exchanged but no validation of the LDAP server's SSL certificate is performed.
->**Note**: Before GitLab 9.5, `validate_certificates: false` is the default if
+>**Note**: Before GitLab 9.5, `verify_certificates: false` is the default if
unspecified.
## Limitations
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 5732b6a1ca4..e3b10119090 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -32,6 +32,14 @@ prometheus_listen_addr = "localhost:9236"
Changes to `/home/git/gitaly/config.toml` are applied when you run `service
gitlab restart`.
+## Client-side GRPC logs
+
+Gitaly uses the [gRPC](https://grpc.io/) RPC framework. The Ruby gRPC
+client has its own log file which may contain useful information when
+you are seeing Gitaly errors. You can control the log level of the
+gRPC client with the `GRPC_LOG_LEVEL` environment variable. The
+default level is `WARN`.
+
## Running Gitaly on its own server
> This is an optional way to deploy Gitaly which can benefit GitLab
@@ -145,8 +153,8 @@ Omnibus installations:
```ruby
# /etc/gitlab/gitlab.rb
git_data_dirs({
- { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
- { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
+ 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' },
+ 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 582f3c67b0d..59305774c4a 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -171,9 +171,9 @@ and [projects APIs](../api/projects.md).
## Implementation details
When GitLab receives an artifacts archive, an archive metadata file is also
-generated. This metadata file describes all the entries that are located in the
-artifacts archive itself. The metadata file is in a binary format, with
-additional GZIP compression.
+generated by [GitLab Workhorse]. This metadata file describes all the entries
+that are located in the artifacts archive itself.
+The metadata file is in a binary format, with additional GZIP compression.
GitLab does not extract the artifacts archive in order to save space, memory
and disk I/O. It instead inspects the metadata file which contains all the
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
new file mode 100644
index 00000000000..bac8fa4bd9d
--- /dev/null
+++ b/doc/administration/raketasks/storage.md
@@ -0,0 +1,107 @@
+# Repository Storage Rake Tasks
+
+This is a collection of rake tasks you can use to help you list and migrate
+existing projects from Legacy storage to the new Hashed storage type.
+
+You can read more about the storage types [here][storage-types].
+
+## List projects on Legacy storage
+
+To have a simple summary of projects using **Legacy** storage:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:legacy_projects
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:legacy_projects
+
+```
+
+------
+
+To list projects using **Legacy** storage:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:list_legacy_projects
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:list_legacy_projects
+
+```
+
+## List projects on Hashed storage
+
+To have a simple summary of projects using **Hashed** storage:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:hashed_projects
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:hashed_projects
+
+```
+
+------
+
+To list projects using **Hashed** storage:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:list_hashed_projects
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:list_hashed_projects
+
+```
+
+## Migrate existing projects to Hashed storage
+
+Before migrating your existing projects, you should
+[enable hashed storage][storage-migration] for the new projects as well.
+
+This task will schedule all your existing projects to be migrated to the
+**Hashed** storage type:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:migrate_to_hashed
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:migrate_to_hashed
+
+```
+
+You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
+There is a specific Queue you can watch to see how long it will take to finish: **project_migrate_hashed_storage**
+
+After it reaches zero, you can confirm every project has been migrated by running the commands above.
+If you find it necessary, you can run this migration script again to schedule missing projects.
+
+Any error or warning will be logged in the sidekiq log file.
+
+
+[storage-types]: ../repository_storage_types.md
+[storage-migration]: ../repository_storage_types.md#how-to-migrate-to-hashed-storage
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index e99a7ee29cc..1304476e678 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -77,6 +77,33 @@ and use [an application password](https://support.google.com/mail/answer/185833)
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
+### Security Concerns
+
+**WARNING:** Be careful when choosing the domain used for receiving incoming
+email.
+
+For the sake of example, suppose your top-level company domain is `hooli.com`.
+All employees in your company have an email address at that domain via Google
+Apps, and your company's private Slack instance requires a valid `@hooli.com`
+email address in order to sign up.
+
+If you also host a public-facing GitLab instance at `hooli.com` and set your
+incoming email domain to `hooli.com`, an attacker could abuse the "Create new
+issue by email" feature by using a project's unique address as the email when
+signing up for Slack, which would send a confirmation email, which would create
+a new issue on the project owned by the attacker, allowing them to click the
+confirmation link and validate their account on your company's private Slack
+instance.
+
+We recommend receiving incoming email on a subdomain, such as
+`incoming.hooli.com`, and ensuring that you do not employ any services that
+authenticate solely based on access to an email domain such as `*.hooli.com.`
+Alternatively, use a dedicated domain for GitLab email communications such as
+`hooli-gitlab.com`.
+
+See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366)
+for a real-world example of this exploit.
+
### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
@@ -141,7 +168,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
```
-
+
```ruby
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
gitlab_rails['incoming_email_enabled'] = true
@@ -253,7 +280,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
idle_timeout: 60
```
-
+
```yaml
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
incoming_email:
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
new file mode 100644
index 00000000000..fa882bbe28a
--- /dev/null
+++ b/doc/administration/repository_storage_types.md
@@ -0,0 +1,69 @@
+# Repository Storage Types
+
+> [Introduced][ce-28283] in GitLab 10.0.
+
+## Legacy Storage
+
+Legacy Storage is the storage behavior prior to version 10.0. For historical reasons, GitLab replicated the same
+mapping structure from the projects URLs:
+
+ * Project's repository: `#{namespace}/#{project_name}.git`
+ * Project's wiki: `#{namespace}/#{project_name}.wiki.git`
+
+This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the
+repository is stored.
+
+On the other hand this has some drawbacks:
+
+Storage location will concentrate huge amount of top-level namespaces. The impact can be reduced by the introduction of [multiple storage paths][storage-paths].
+
+Because Backups are a snapshot of the same URL mapping, if you try to recover a very old backup, you need to verify
+if any project has taken the place of an old removed project sharing the same URL. This means that `mygroup/myproject`
+from your backup may not be the same original project that is today in the same URL.
+
+Any change in the URL will need to be reflected on disk (when groups / users or projects are renamed). This can add a lot
+of load in big installations, and can be even worst if they are using any type of network based filesystem.
+
+Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct
+order or we may end-up with wrong repository or missing data temporarily.
+
+## Hashed Storage
+
+Hashed Storage is the new storage behavior we are rolling out with 10.0. It's not enabled by default yet, but we
+encourage everyone to try-it and take the time to fix any script you may have that depends on the old behavior.
+
+Instead of coupling project URL and the folder structure where the repository will be stored on disk, we are coupling
+a hash, based on the project's ID.
+
+This makes the folder structure immutable, and therefore eliminates any requirement to synchronize state from URLs to
+disk structure. This means that renaming a group, user or project will cost only the database transaction, and will take
+effect immediately.
+
+The hash also helps to spread the repositories more evenly on the disk, so the top-level directory will contain less
+folders than the total amount of top-level namespaces.
+
+Hash format is based on hexadecimal representation of SHA256: `SHA256(project.id)`.
+Top-level folder uses first 2 characters, followed by another folder with the next 2 characters. They are both stored in
+a special folder `@hashed`, to co-exist with existing Legacy projects:
+
+```ruby
+# Project's repository:
+"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git"
+
+# Wiki's repository:
+"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.wiki.git"
+```
+
+This new format also makes possible to restore backups with confidence, as when restoring a repository from the backup,
+you will never mistakenly restore a repository in the wrong project (considering the backup is made after the migration).
+
+### How to migrate to Hashed Storage
+
+In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
+"_Create new projects using hashed storage paths_".
+
+To migrate your existing projects to the new storage type, check the specific [rake tasks].
+
+[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
+[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
+[storage-paths]: repository_storage_types.md
diff --git a/doc/api/README.md b/doc/api/README.md
index 6cbea29bda6..3fd4c97e536 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -14,6 +14,7 @@ following locations:
- [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md)
- [Commits](commits.md)
+- [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Environments](environments.md)
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 2a78553782f..5a4a8d888b3 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -181,6 +181,12 @@ Example response:
"parent_ids": [
"ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
],
+ "last_pipeline" : {
+ "id": 8,
+ "ref": "master",
+ "sha": "2dc6aa325a317eda67812f05600bdf0fcdc70ab0"
+ "status": "created"
+ }
"stats": {
"additions": 15,
"deletions": 10,
diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md
new file mode 100644
index 00000000000..8b26f7093ab
--- /dev/null
+++ b/doc/api/custom_attributes.md
@@ -0,0 +1,105 @@
+# Custom Attributes API
+
+Every API call to custom attributes must be authenticated as administrator.
+
+## List custom attributes
+
+Get all custom attributes on a user.
+
+```
+GET /users/:id/custom_attributes
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes
+```
+
+Example response:
+
+```json
+[
+ {
+ "key": "location",
+ "value": "Antarctica"
+ },
+ {
+ "key": "role",
+ "value": "Developer"
+ }
+]
+```
+
+## Single custom attribute
+
+Get a single custom attribute on a user.
+
+```
+GET /users/:id/custom_attributes/:key
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a user |
+| `key` | string | yes | The key of the custom attribute |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
+```
+
+Example response:
+
+```json
+{
+ "key": "location",
+ "value": "Antarctica"
+}
+```
+
+## Set custom attribute
+
+Set a custom attribute on a user. The attribute will be updated if it already exists,
+or newly created otherwise.
+
+```
+PUT /users/:id/custom_attributes/:key
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a user |
+| `key` | string | yes | The key of the custom attribute |
+| `value` | string | yes | The value of the custom attribute |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "value=Greenland" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
+```
+
+Example response:
+
+```json
+{
+ "key": "location",
+ "value": "Greenland"
+}
+```
+
+## Delete custom attribute
+
+Delete a custom attribute on a user.
+
+```
+DELETE /users/:id/custom_attributes/:key
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a user |
+| `key` | string | yes | The key of the custom attribute |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 8ca66049d31..ec8ff3cd3f3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -95,6 +95,7 @@ Example response:
"username" : "root"
},
"updated_at" : "2016-01-04T15:31:51.081Z",
+ "closed_at" : null,
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
@@ -109,7 +110,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -205,6 +207,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
+ "closed_at" : null,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
@@ -214,7 +217,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -311,6 +315,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
+ "closed_at" : "2016-01-05T15:31:46.176Z",
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
@@ -320,7 +325,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -358,7 +364,8 @@ Example response:
"id" : 11,
"title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z",
- "updated_at" : "2016-01-04T15:31:39.788Z"
+ "updated_at" : "2016-01-04T15:31:39.788Z",
+ "closed_at" : "2016-01-05T15:31:46.176Z"
},
"author" : {
"state" : "active",
@@ -403,6 +410,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -465,6 +473,7 @@ Example response:
},
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
+ "closed_at" : null,
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
@@ -477,6 +486,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -510,6 +520,8 @@ PUT /projects/:id/issues/:issue_iid
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
+
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
@@ -533,6 +545,7 @@ Example response:
"project_id" : 4,
"description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z",
+ "closed_at" : "2016-01-08T12:55:16.213Z",
"iid" : 15,
"labels" : [
"bug"
@@ -552,6 +565,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -615,6 +629,7 @@ Example response:
"state": "opened",
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
+ "closed_at": null,
"labels": [],
"milestone": null,
"assignees": [{
@@ -650,6 +665,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -692,6 +708,7 @@ Example response:
"state": "opened",
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
+ "closed_at": null,
"labels": [],
"milestone": null,
"assignees": [{
@@ -727,6 +744,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -757,6 +775,44 @@ POST /projects/:id/issues/:issue_iid/unsubscribe
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
```
+Example response:
+
+```json
+{
+ "id": 93,
+ "iid": 12,
+ "project_id": 5,
+ "title": "Incidunt et rerum ea expedita iure quibusdam.",
+ "description": "Et cumque architecto sed aut ipsam.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:41:45.217Z",
+ "updated_at": "2016-04-07T13:02:37.905Z",
+ "labels": [],
+ "milestone": null,
+ "assignee": {
+ "name": "Edwardo Grady",
+ "username": "keyon",
+ "id": 21,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/keyon"
+ },
+ "author": {
+ "name": "Vivian Hermann",
+ "username": "orville",
+ "id": 11,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/orville"
+ },
+ "subscribed": false,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/12",
+ "confidential": false,
+ "discussion_locked": false
+}
+```
+
## Create a todo
Manually creates a todo for the current user on an issue. If
@@ -849,7 +905,8 @@ Example response:
"downvotes": 0,
"due_date": null,
"web_url": "http://example.com/example/example/issues/110",
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index d60c7c12881..e7060e154f4 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -336,7 +336,7 @@ Parameters
| Attribute | Type | Required | Description |
|-------------|---------|----------|-------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `ref_name` | string | yes | The ref from a repository |
+| `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
| `job` | string | yes | The name of the job |
Example request:
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index bff8a2d3e4d..50a971102fb 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced
GET /projects/:id/merge_requests?my_reaction_emoji=star
```
+`project_id` represents the ID of the project where the MR resides.
+`project_id` will always equal `target_project_id`.
+
+In the case of a merge request from the same project,
+`source_project_id`, `target_project_id` and `project_id`
+will be the same. In the case of a merge request from a fork,
+`target_project_id` and `project_id` will be the same and
+`source_project_id` will be the fork project's ID.
+
Parameters:
| Attribute | Type | Required | Description |
@@ -192,6 +201,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -267,6 +277,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -378,6 +389,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -471,6 +483,7 @@ POST /projects/:id/merge_requests
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -500,6 +513,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
Must include at least one non-required attribute from above.
@@ -554,6 +568,7 @@ Must include at least one non-required attribute from above.
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -658,6 +673,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -734,6 +750,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -1028,7 +1045,8 @@ Example response:
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/francisca"
+ "web_url": "https://gitlab.example.com/francisca",
+ "discussion_locked": false
},
"assignee": {
"name": "Dr. Gabrielle Strosin",
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 3144220e588..07331d05231 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -635,6 +635,98 @@ POST /projects/:id/fork
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
+## List Forks of a project
+
+>**Note:** This feature was introduced in GitLab 10.1
+
+List the projects accessible to the calling user that have an established, forked relationship with the specified project
+
+```
+GET /projects/:id/forks
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `owned` | boolean | no | Limit by projects owned by the current user |
+| `membership` | boolean | no | Limit by projects that the current user is a member of |
+| `starred` | boolean | no | Limit by projects starred by the current user |
+| `statistics` | boolean | no | Include project statistics |
+| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
+| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/forks"
+```
+
+Example responses:
+
+```json
+[
+ {
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "visibility": "internal",
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "jobs_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "resolve_outdated_diff_discussions": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "id": 3,
+ "name": "Diaspora",
+ "path": "diaspora",
+ "kind": "group",
+ "full_path": "diaspora"
+ },
+ "import_status": "none",
+ "archived": true,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 1,
+ "public_jobs": true,
+ "shared_with_groups": [],
+ "only_allow_merge_if_pipeline_succeeds": false,
+ "only_allow_merge_if_all_discussions_are_resolved": false,
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
+ }
+]
+```
+
## Star a project
Stars a given project. Returns status code `304` if the project is already starred.
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 32fe5eea692..bebe6536b6e 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -131,7 +131,7 @@ Parameters:
"message": null
}
```
-The message will be `nil` when creating a lightweight tag otherwise
+The message will be `null` when creating a lightweight tag otherwise
it will contain the annotation.
In case of an error,
diff --git a/doc/api/users.md b/doc/api/users.md
index 6d5db16b36a..1643c584244 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -154,6 +154,12 @@ You can search users by creation date time range with:
GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060
```
+You can filter by [custom attributes](custom_attributes.md) with:
+
+```
+GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value
+```
+
## Single user
Get a single user.
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 10eebc4a3c1..15ce5f96b60 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -155,3 +155,5 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi
```
On success the HTTP status code is `204` and no JSON response is expected.
+
+[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index f28c9791bee..4586caa457d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -250,6 +250,8 @@ By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
copies the filesystem on every run. This is a very disk-intensive operation
which can be avoided if a different driver is used, for example `overlay2`.
+### Requirements
+
1. Make sure a recent kernel is used, preferably `>= 4.2`.
1. Check whether the `overlay` module is loaded:
@@ -271,14 +273,27 @@ which can be avoided if a different driver is used, for example `overlay2`.
overlay
```
-1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
+### Use driver per project
- ```
- variables:
- DOCKER_DRIVER: overlay2
- ```
-
-> **Note:**
+You can enable the driver for each project individually by editing the project's `.gitlab-ci.yml`:
+
+```
+variables:
+ DOCKER_DRIVER: overlay2
+```
+
+### Use driver for every project
+
+To enable the driver for every project, you can set the environment variable for every build by adding `environment` in the `[[runners]]` section of `config.toml`:
+
+```toml
+environment = ["DOCKER_DRIVER=overlay2"]
+```
+
+If you're running multiple Runners you will have to modify all configuration files.
+
+> **Notes:**
+- More information about the Runner configuration is available in the [Runner documentation](https://docs.gitlab.com/runner/configuration/).
- For more information about using OverlayFS with Docker, you can read
[Use the OverlayFS storage driver](https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/).
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 796a025b951..b8f9988e3ef 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,51 +1,46 @@
-## Enable or disable GitLab CI
+## Enable or disable GitLab CI/CD
-_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
+To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
file present at the root directory of your project and a
[runner](runners/README.md) properly set up. You can read our
-[quick start guide](quick_start/README.md) to get you started._
+[quick start guide](quick_start/README.md) to get you started.
-If you are using an external CI server like Jenkins or Drone CI, it is advised
-to disable GitLab CI in order to not have any conflicts with the commits status
+If you are using an external CI/CD server like Jenkins or Drone CI, it is advised
+to disable GitLab CI/CD in order to not have any conflicts with the commits status
API.
---
-GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project.
-Disabling GitLab CI in a project does not delete any previous jobs.
-In fact, the `/pipelines` and `/builds` pages can still be accessed, although
+GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project.
+Disabling GitLab CI/CD in a project does not delete any previous jobs.
+In fact, the `/pipelines` and `/jobs` pages can still be accessed, although
it's hidden from the left sidebar menu.
-GitLab CI is enabled by default on new installations and can be disabled either
+GitLab CI/CD is enabled by default on new installations and can be disabled either
individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively.
### Per-project user setting
-The setting to enable or disable GitLab CI can be found with the name **Pipelines**
-under the **Sharing & Permissions** area of a project's settings along with
-**Merge Requests**. Choose one of **Disabled**, **Only team members** and
-**Everyone with access** and hit **Save changes** for the settings to take effect.
+The setting to enable or disable GitLab CI/CD can be found under your project's
+**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
+or "Everyone with access" and hit **Save changes** for the settings to take effect.
-![Sharing & Permissions settings](img/permissions_settings.png)
+![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
----
-
-### Site-wide administrator setting
+### Site-wide admin setting
-You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml`
+You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
and `gitlab.rb` for source and Omnibus installations respectively.
Two things to note:
-1. Disabling GitLab CI, will affect only newly-created projects. Projects that
+1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that
had it enabled prior to this modification, will work as before.
-1. Even if you disable GitLab CI, users will still be able to enable it in the
+1. Even if you disable GitLab CI/CD, users will still be able to enable it in the
project's settings.
----
-
For installations from source, open `gitlab.yml` with your editor and set
`builds` to `false`:
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index cbf06afa294..c03e16b1b38 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your
servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
enabled for your project, you can use it to assist with your deployments, and
-can even access a web terminal for your environment from within GitLab!
+can even access a [web terminal](#web-terminals) for your environment from within GitLab!
To better understand how environments and deployments work, let's consider an
example. We assume that you have already created a project in GitLab and set up
@@ -119,7 +119,7 @@ where you can find information of the last deployment status of an environment.
Here's how the Environments page looks so far.
-![Staging environment view](img/environments_available_staging.png)
+![Environment view](img/environments_available.png)
There's a bunch of information there, specifically you can see:
@@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views.
| Pipelines | Single pipeline | Environments | Deployments | jobs |
| --------- | ----------------| ------------ | ----------- | -------|
-| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
+| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_jobs.png) |
Clicking on the play button in either of these places will trigger the
`deploy_prod` job, and the deployment will be recorded under a new
@@ -240,55 +240,18 @@ Remember that if your environment's name is `production` (all lowercase), then
it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
Double the benefit!
-## Web terminals
-
->**Note:**
-Web terminals were added in GitLab 8.15 and are only available to project
-masters and owners.
-
-If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes service][kubernetes-service], GitLab can open
-a terminal session to your environment! This is a very powerful feature that
-allows you to debug issues without leaving the comfort of your web browser. To
-enable it, just follow the instructions given in the service documentation.
-
-Once enabled, your environments will gain a "terminal" button:
-
-![Terminal button on environment index](img/environments_terminal_button_on_index.png)
-
-You can also access the terminal button from the page for a specific environment:
-
-![Terminal button for an environment](img/environments_terminal_button_on_show.png)
-
-Wherever you find it, clicking the button will take you to a separate page to
-establish the terminal session:
-
-![Terminal page](img/environments_terminal_page.png)
-
-This works just like any other terminal - you'll be in the container created
-by your deployment, so you can run shell commands and get responses in real
-time, check the logs, try out configuration or code tweaks, etc. You can open
-multiple terminals to the same environment - they each get their own shell
-session - and even a multiplexer like `screen` or `tmux`!
-
->**Note:**
-Container-based deployments often lack basic tools (like an editor), and may
-be stopped or restarted at any time. If this happens, you will lose all your
-changes! Treat this as a debugging tool, not a comprehensive online IDE.
-
----
-
-While this is fine for deploying to some stable environments like staging or
-production, what happens for branches? So far we haven't defined anything
-regarding deployments for branches other than `master`. Dynamic environments
-will help us achieve that.
-
## Dynamic environments
As the name suggests, it is possible to create environments on the fly by just
declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
the basis of [Review apps](review_apps/index.md).
+>**Note:**
+The `name` and `url` parameters can use any of the defined CI variables,
+including predefined, secure variables and `.gitlab-ci.yml`
+[`variables`](yaml/README.md#variables).
+You however cannot use variables defined under `script` or on the Runner's side.
+
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
our example which will deploy to all branches except `master`:
@@ -434,11 +397,12 @@ Let's briefly see where URL that's defined in the environments is exposed.
## Making use of the environment URL
-The environment URL is exposed in a few places within GitLab.
+The [environment URL](yaml/README.md#environments-url) is exposed in a few
+places within GitLab.
| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
| -------------------- | ------------ | ----------- |
-| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) |
+| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_available.png) | ![Environment URL in deployments](img/deployments_view.png) |
If a merge request is eventually merged to the default branch (in our case
`master`) and that branch also deploys to an environment (in our case `staging`
@@ -446,8 +410,7 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
-### <a name="route-map"></a>Go directly from source files to public pages on the environment
-
+### Go directly from source files to public pages on the environment
> Introduced in GitLab 8.17.
@@ -599,7 +562,7 @@ exist, you should see something like:
>**Notes:**
>
-- For the monitor dashboard to appear, you need to:
+- For the monitoring dashboard to appear, you need to:
- Have enabled the [Prometheus integration][prom]
- Configured Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/metrics.md)
- With GitLab 9.2, all deployments to an environment are shown directly on the
@@ -609,10 +572,9 @@ If you have enabled [Prometheus for monitoring system and response metrics](http
Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any
environment which has had a successful deployment. If monitoring data was
-successfully retrieved, a Monitoring button will appear on the environment's
-detail page.
+successfully retrieved, a Monitoring button will appear for each environment.
-![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
+![Environment Detail with Metrics](img/deployments_view.png)
Clicking on the Monitoring button will display a new page, showing up to the last
8 hours of performance data. It may take a minute or two for data to appear
@@ -624,6 +586,50 @@ version of the app, all without leaving GitLab.
![Monitoring dashboard](img/environments_monitoring.png)
+## Web terminals
+
+>**Note:**
+Web terminals were added in GitLab 8.15 and are only available to project
+masters and owners.
+
+If you deploy to your environments with the help of a deployment service (e.g.,
+the [Kubernetes service][kubernetes-service]), GitLab can open
+a terminal session to your environment! This is a very powerful feature that
+allows you to debug issues without leaving the comfort of your web browser. To
+enable it, just follow the instructions given in the service integration
+documentation.
+
+Once enabled, your environments will gain a "terminal" button:
+
+![Terminal button on environment index](img/environments_terminal_button_on_index.png)
+
+You can also access the terminal button from the page for a specific environment:
+
+![Terminal button for an environment](img/environments_terminal_button_on_show.png)
+
+Wherever you find it, clicking the button will take you to a separate page to
+establish the terminal session:
+
+![Terminal page](img/environments_terminal_page.png)
+
+This works just like any other terminal - you'll be in the container created
+by your deployment, so you can run shell commands and get responses in real
+time, check the logs, try out configuration or code tweaks, etc. You can open
+multiple terminals to the same environment - they each get their own shell
+session - and even a multiplexer like `screen` or `tmux`!
+
+>**Note:**
+Container-based deployments often lack basic tools (like an editor), and may
+be stopped or restarted at any time. If this happens, you will lose all your
+changes! Treat this as a debugging tool, not a comprehensive online IDE.
+
+---
+
+While this is fine for deploying to some stable environments like staging or
+production, what happens for branches? So far we haven't defined anything
+regarding deployments for branches other than `master`. Dynamic environments
+will help us achieve that.
+
## Checkout deployments locally
Since 8.13, a reference in the git repository is saved for each deployment, so
diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png
deleted file mode 100644
index 2d7eec8a949..00000000000
--- a/doc/ci/img/builds_tab.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png
index 7ded0c97b72..436fed5f465 100644
--- a/doc/ci/img/deployments_view.png
+++ b/doc/ci/img/deployments_view.png
Binary files differ
diff --git a/doc/ci/img/environments_available.png b/doc/ci/img/environments_available.png
new file mode 100644
index 00000000000..2991a309655
--- /dev/null
+++ b/doc/ci/img/environments_available.png
Binary files differ
diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png
deleted file mode 100644
index 5c031ad0d9d..00000000000
--- a/doc/ci/img/environments_available_staging.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png
index 0f42b368c5b..45124b3d8d8 100644
--- a/doc/ci/img/environments_dynamic_groups.png
+++ b/doc/ci/img/environments_dynamic_groups.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png
deleted file mode 100644
index 44010f6aa6f..00000000000
--- a/doc/ci/img/environments_link_url.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png
deleted file mode 100644
index 4f90143527a..00000000000
--- a/doc/ci/img/environments_link_url_deployments.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png
index 64f134e0b0d..7ce46063062 100644
--- a/doc/ci/img/environments_link_url_mr.png
+++ b/doc/ci/img/environments_link_url_mr.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png
deleted file mode 100644
index e7cf63a1031..00000000000
--- a/doc/ci/img/environments_manual_action_builds.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png
index 2b3f6f3edad..93beaa0de54 100644
--- a/doc/ci/img/environments_manual_action_deployments.png
+++ b/doc/ci/img/environments_manual_action_deployments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png
index e0c07604e7f..9490be63f14 100644
--- a/doc/ci/img/environments_manual_action_environments.png
+++ b/doc/ci/img/environments_manual_action_environments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_jobs.png b/doc/ci/img/environments_manual_action_jobs.png
new file mode 100644
index 00000000000..9ae223cf77f
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_jobs.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png
index 82bbae88027..129e44f6fb0 100644
--- a/doc/ci/img/environments_manual_action_pipelines.png
+++ b/doc/ci/img/environments_manual_action_pipelines.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png
index 36337cb1870..1eeb4379eb7 100644
--- a/doc/ci/img/environments_manual_action_single_pipeline.png
+++ b/doc/ci/img/environments_manual_action_single_pipeline.png
Binary files differ
diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png
index d9c46ea4c95..dcffdd1fdb8 100644
--- a/doc/ci/img/environments_monitoring.png
+++ b/doc/ci/img/environments_monitoring.png
Binary files differ
diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png
index 7bff84362a3..4bb643d708f 100644
--- a/doc/ci/img/environments_mr_review_app.png
+++ b/doc/ci/img/environments_mr_review_app.png
Binary files differ
diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png
index 6f05b2aa343..061bb7c3c87 100644
--- a/doc/ci/img/environments_terminal_button_on_index.png
+++ b/doc/ci/img/environments_terminal_button_on_index.png
Binary files differ
diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png
index 9469fab99ab..4d24304bc93 100644
--- a/doc/ci/img/environments_terminal_button_on_show.png
+++ b/doc/ci/img/environments_terminal_button_on_show.png
Binary files differ
diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png
deleted file mode 100644
index 821352188ef..00000000000
--- a/doc/ci/img/environments_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/permissions_settings.png b/doc/ci/img/permissions_settings.png
deleted file mode 100644
index 1454c75fd24..00000000000
--- a/doc/ci/img/permissions_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/prometheus_environment_detail_with_metrics.png b/doc/ci/img/prometheus_environment_detail_with_metrics.png
deleted file mode 100644
index 214b10624a9..00000000000
--- a/doc/ci/img/prometheus_environment_detail_with_metrics.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 88e53ff40e8..2d56b2540ef 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -106,7 +106,7 @@ What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-a "CI Lint" button to go to this page under **Pipelines âž” Pipelines** and
+a "CI Lint" button to go to this page under **CI/CD âž” Pipelines** and
**Pipelines âž” Jobs** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
-**Settings âž” Pipelines**. Setting up a Runner is easy and straightforward. The
+**Settings âž” CI/CD**. Setting up a Runner is easy and straightforward. The
official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>.
@@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your
-project, following **Settings âž” Pipelines**.
+project, following **Settings âž” CI/CD**.
![Activated runners](img/runners_activated.png)
@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project.
To enable the **Shared Runners** you have to go to your project's
-**Settings âž” Pipelines** and click **Enable shared runners**.
+**Settings âž” CI/CD** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index f5d3b524d6e..8b51d112a2c 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -35,7 +35,7 @@ are:
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
-under **Settings âž” Pipelines**.
+under **Settings âž” CI/CD**.
Projects with high demand of CI activity can also benefit from using specific
Runners. By having dedicated Runners you are guaranteed that the Runner is not
@@ -61,7 +61,7 @@ You can only register a shared Runner if you are an admin of the GitLab instance
Shared Runners are enabled by default as of GitLab 8.2, but can be disabled
with the **Disable shared Runners** button which is present under each project's
-**Settings âž” Pipelines** page. Previous versions of GitLab defaulted shared
+**Settings âž” CI/CD** page. Previous versions of GitLab defaulted shared
Runners to disabled.
## Registering a specific Runner
@@ -76,7 +76,7 @@ Registering a specific can be done in two ways:
To create a specific Runner without having admin rights to the GitLab instance,
visit the project you want to make the Runner work for in GitLab:
-1. Go to **Settings âž” Pipelines** to obtain the token
+1. Go to **Settings âž” CI/CD** to obtain the token
1. [Register the Runner][register]
### Making an existing shared Runner specific
@@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings.
To lock/unlock a Runner:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish to lock/unlock and make sure it's enabled
1. Click the pencil button
1. Check the **Lock to current projects** option
@@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis
To enable/disable a Runner in your project:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish to enable/disable
1. Click **Enable for this project** or **Disable for this project**
@@ -136,7 +136,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on
To protect/unprotect Runners:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find a Runner you want to protect/unprotect and make sure it's enabled
1. Click the pencil button besides the Runner name
1. Check the **Protected** option
@@ -220,7 +220,7 @@ each Runner's settings.
To make a Runner pick tagged/untagged jobs:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish and make sure it's enabled
1. Click the pencil button
1. Check the **Run untagged jobs** option
@@ -228,7 +228,8 @@ To make a Runner pick tagged/untagged jobs:
### Be careful with sensitive information
-If you can run a job on a Runner, you can get access to any code it runs
+With some [Runner Executors](https://docs.gitlab.com/runner/executors/README.html),
+if you can run a job on the Runner, you can get access to any code it runs
and get the token of the Runner. With shared Runners, this means that anyone
that runs jobs on the Runner, can access anyone else's code that runs on the
Runner.
@@ -237,7 +238,8 @@ In addition, because you can get access to the Runner token, it is possible
to create a clone of a Runner and submit false jobs, for example.
The above is easily avoided by restricting the usage of shared Runners
-on large public GitLab instances and controlling access to your GitLab instance.
+on large public GitLab instances, controlling access to your GitLab instance,
+and using more secure [Runner Executors](https://docs.gitlab.com/runner/executors/README.html).
### Forks
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index cdb9858e179..e5a2bbd1773 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -34,7 +34,7 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a
passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab
-following **Settings > Pipelines** and look for the "Secret Variables" section.
+following **Settings > CI/CD** and look for the "Secret Variables" section.
As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
content of your _private_ key that you created earlier.
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 7ec7136d8c6..56a16f77e7f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -19,7 +19,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new
## Adding a new trigger
You can add a new trigger by going to your project's
-**Settings âž” Pipelines** under **Triggers**. The **Add trigger** button will
+**Settings âž” CI/CD** under **Triggers**. The **Add trigger** button will
create a new token which you can then use to trigger a rerun of this
particular project's pipeline.
@@ -43,7 +43,7 @@ From now on the trigger will be run as you.
## Revoking a trigger
You can revoke a trigger any time by going at your project's
-**Settings âž” Pipelines** under **Triggers** and hitting the **Revoke** button.
+**Settings âž” CI/CD** under **Triggers** and hitting the **Revoke** button.
The action is irreversible.
## Triggering a pipeline
@@ -64,7 +64,7 @@ POST /projects/:id/trigger/pipeline
The required parameters are the [trigger's `token`](#authentication-tokens)
and the Git `ref` on which the trigger will be performed. Valid refs are the
branch and the tag. The `:id` of a project can be found by
-[querying the API](../../api/projects.md) or by visiting the **Pipelines**
+[querying the API](../../api/projects.md) or by visiting the **CI/CD**
settings page which provides self-explanatory examples.
When a rerun of a pipeline is triggered, the information is exposed in GitLab's
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6513b31826a..17839cbaef1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -149,28 +149,31 @@ script:
## Secret variables
->**Notes:**
-- This feature requires GitLab Runner 0.4.0 or higher.
-- Group-level secret variables added in GitLab 9.4.
-- Be aware that secret variables are not masked, and their values can be shown
- in the job logs if explicitly asked to do so. If your project is public or
- internal, you can set the pipelines private from your project's Pipelines
- settings. Follow the discussion in issue [#13784][ce-13784] for masking the
- secret variables.
-
-GitLab CI allows you to define per-project or per-group **secret variables**
-that are set in the build environment. The secret variables are stored out of
-the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner
-making them available in the build environment. It's the recommended method to
-use for storing things like passwords, secret keys and credentials.
+NOTE: **Note:**
+Group-level secret variables were added in GitLab 9.4.
+
+CAUTION: **Important:**
+Be aware that secret variables are not masked, and their values can be shown
+in the job logs if explicitly asked to do so. If your project is public or
+internal, you can set the pipelines private from your [project's Pipelines
+settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
+Follow the discussion in issue [#13784][ce-13784] for masking the secret variables.
+
+GitLab CI allows you to define per-project or per-group secret variables
+that are set in the pipeline environment. The secret variables are stored out of
+the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
+making them available during a pipeline run. It's the recommended method to
+use for storing things like passwords, SSH keys and credentials.
Project-level secret variables can be added by going to your project's
-**Settings âž” Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Likewise, group-level secret variables can be added by going to your group's
-**Settings âž” Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Any variables of [subgroups] will be inherited recursively.
+![Secret variables](img/secret_variables.png)
+
Once you set them, they will be available for all subsequent pipelines. You can also
[protect your variables](#protected-secret-variables).
@@ -185,8 +188,8 @@ protected, it would only be securely passed to pipelines running on the
protected variables.
Protected variables can be added by going to your project's
-**Settings âž” Pipelines**, then finding the section called
-**Secret variables**, and check *Protected*.
+**Settings > CI/CD**, then finding the section called
+**Secret variables**, and check "Protected".
Once you set them, they will be available for all subsequent pipelines.
@@ -202,7 +205,7 @@ are set in the build environment. These variables are only defined for
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
-[Kubernetes Service](../../user/project/integrations/kubernetes.md).
+[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables).
## Debug tracing
@@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token"
export CI_REGISTRY_PASSWORD="longalfanumstring"
```
-[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784
+[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
[eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
diff --git a/doc/ci/variables/img/secret_variables.png b/doc/ci/variables/img/secret_variables.png
new file mode 100644
index 00000000000..f70935069d9
--- /dev/null
+++ b/doc/ci/variables/img/secret_variables.png
Binary files differ
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index d0ac3ec6163..38bd0450a09 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -252,6 +252,8 @@ The `cache:key` variable can use any of the [predefined variables](../variables/
The default key is **default** across the project, therefore everything is
shared between each pipelines and jobs by default, starting from GitLab 9.0.
+>**Note:** The `cache:key` variable cannot contain the `/` character.
+
---
**Example configurations**
@@ -276,7 +278,7 @@ To enable per-job and per-branch caching:
```yaml
cache:
- key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME"
+ key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -284,7 +286,7 @@ To enable per-branch and per-stage caching:
```yaml
cache:
- key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME"
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -293,7 +295,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%"
+ key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
untracked: true
```
@@ -302,7 +304,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
```yaml
cache:
- key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
+ key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
untracked: true
```
@@ -727,6 +729,9 @@ deployment to the `production` environment.
- Before GitLab 8.11, the name of an environment could be defined as a string like
`environment: production`. The recommended way now is to define it under the
`name` keyword.
+- The `name` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
The `environment` name can contain:
@@ -762,6 +767,9 @@ deploy to production:
- Introduced in GitLab 8.11.
- Before GitLab 8.11, the URL could be added only in GitLab's UI. The
recommended way now is to define it in `.gitlab-ci.yml`.
+- The `url` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
This is an optional value that when set, it exposes buttons in various places
in GitLab which when clicked take you to the defined URL.
@@ -841,10 +849,9 @@ The `stop_review_app` job is **required** to have the following keywords defined
**Notes:**
- [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
- The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15.
-
-`environment` can also represent a configuration hash with `name` and `url`.
-These parameters can use any of the defined [CI variables](#variables)
-(including predefined, secure variables and `.gitlab-ci.yml` variables).
+- The `name` and `url` parameters can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
For example:
@@ -1366,25 +1373,31 @@ variables:
GIT_DEPTH: "3"
```
-## Hidden keys
+## Hidden keys (jobs)
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
-use this feature to ignore jobs, or use the
-[special YAML features](#special-yaml-features) and transform the hidden keys
-into templates.
+If you want to temporarily 'disable' a job, rather than commenting out all the
+lines where the job is defined:
+
+```
+#hidden_job:
+# script:
+# - run test
+```
-In the following example, `.key_name` will be ignored:
+you can instead start its name with a dot (`.`) and it will not be processed by
+GitLab CI. In the following example, `.hidden_job` will be ignored:
```yaml
-.key_name:
+.hidden_job:
script:
- - rake spec
+ - run test
```
-Hidden keys can be hashes like normal CI jobs, but you are also allowed to use
-different types of structures to leverage special YAML features.
+Use this feature to ignore jobs, or use the
+[special YAML features](#special-yaml-features) and transform the hidden keys
+into templates.
## Special YAML features
@@ -1400,7 +1413,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
YAML has a handy feature called 'anchors', which lets you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
-properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
+properties, and is a perfect example to be used with [hidden keys](#hidden-keys-jobs)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
@@ -1557,6 +1570,11 @@ Read more on [GitLab Pages user documentation](../../user/project/pages/index.md
Each instance of GitLab CI has an embedded debug tool called Lint.
You can find the link under `/ci/lint` of your gitlab instance.
+## Using reserved keywords
+
+If you get validation error when using specific values (e.g., `true` or `false`),
+try to quote them, or change them to a different form (e.g., `/bin/true`).
+
## Skipping jobs
If your commit message contains `[ci skip]` or `[skip ci]`, using any
diff --git a/doc/development/README.md b/doc/development/README.md
index dd150421b65..b648c7ce086 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -43,6 +43,8 @@
- [Object state models](object_state_models.md)
- [Building a package for testing purposes](build_test_package.md)
- [Manage feature flags](feature_flags.md)
+- [View sent emails or preview mailers](emails.md)
+- [Working with Gitaly](gitaly.md)
## Databases
@@ -60,10 +62,13 @@
- [Ordering Table Columns](ordering_table_columns.md)
- [Verifying Database Capabilities](verifying_database_capabilities.md)
- [Hash Indexes](hash_indexes.md)
+- [Swapping Tables](swapping_tables.md)
-## i18n
+## Internationalization (i18n)
-- [Internationalization for GitLab](i18n_guide.md)
+- [Introduction](i18n/index.md)
+- [Externalization](i18n/externalization.md)
+- [Translation](i18n/translation.md)
## Compliance
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index f83a60e49e8..5452b0e7a2f 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -215,14 +215,29 @@ same time will ensure that both existing and new data is migrated.
In the next release we can remove the `after_commit` hooks and related code. We
will also need to add a post-deployment migration that consumes any remaining
-jobs. Such a migration would look like this:
+jobs and manually run on any un-migrated rows. Such a migration would look like
+this:
```ruby
class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
disable_ddl_transaction!
+ class Service < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'services'
+ end
+
def up
+ # This must be included
Gitlab::BackgroundMigration.steal('ExtractServicesUrl')
+
+ # This should be included, but can be skipped - see below
+ Service.where(url: nil).each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::ExtractServicesUrl.new.perform(*range)
+ end
end
def down
@@ -230,6 +245,15 @@ class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
end
```
+The final step runs for any un-migrated rows after all of the jobs have been
+processed. This is in case a Sidekiq process running the background migrations
+received SIGKILL, leading to the jobs being lost. (See
+[more reliable Sidekiq queue][reliable-sidekiq] for more information.)
+
+If the application does not depend on the data being 100% migrated (for
+instance, the data is advisory, and not mission-critical), then this final step
+can be skipped.
+
This migration will then process any jobs for the ExtractServicesUrl migration
and continue once all jobs have been processed. Once done you can safely remove
the `services.properties` column.
@@ -254,6 +278,9 @@ for more details.
1. Make sure that background migration jobs are idempotent.
1. Make sure that tests you write are not false positives.
+1. Make sure that if the data being migrated is critical and cannot be lost, the
+ clean-up migration also checks the final state of the data before completing.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
+[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 64a89976300..7165b8062a7 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -9,8 +9,18 @@ There are a few rules to get your merge request accepted:
**approved by a [backend maintainer][projects]**.
1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][projects]**.
+ 1. If your merge request includes UX changes [^1], it must
+ be **approved by a [UX team member][team]**.
+ 1. If your merge request includes adding a new JavaScript library [^1], it must be
+ **approved by a [frontend lead][team]**.
+ 1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
+ **approved by a [UX lead][team]**.
1. If your merge request includes frontend and backend changes [^1], it must
be **approved by a [frontend and a backend maintainer][projects]**.
+ 1. If your merge request includes UX and frontend changes [^1], it must
+ be **approved by a [UX team member and a frontend maintainer][team]**.
+ 1. If your merge request includes UX, frontend and backend changes [^1], it must
+ be **approved by a [UX team member, a frontend and a backend maintainer][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must
be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details.
1. To lower the amount of merge requests maintainers need to review, you can
diff --git a/doc/development/emails.md b/doc/development/emails.md
new file mode 100644
index 00000000000..18f47f44cb5
--- /dev/null
+++ b/doc/development/emails.md
@@ -0,0 +1,23 @@
+# Dealing with email in development
+
+## Sent emails
+
+To view rendered emails "sent" in your development instance, visit
+[`/rails/letter_opener`](http://localhost:3000/rails/letter_opener).
+
+## Mailer previews
+
+Rails provides a way to preview our mailer templates in HTML and plaintext using
+dummy data.
+
+The previews live in [`spec/mailers/previews`][previews] and can be viewed at
+[`/rails/mailers`](http://localhost:3000/rails/mailers).
+
+See the [Rails guides] for more info.
+
+[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews
+[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
new file mode 100644
index 00000000000..a76e978bd26
--- /dev/null
+++ b/doc/development/fe_guide/icons.md
@@ -0,0 +1,40 @@
+# Icons
+
+We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages.
+
+### Usage in HAML/Rails
+
+To use a sprite Icon in HAML or Rails we use a specific helper function :
+
+`sprite_icon(icon_name, size: nil, css_class: '')`
+
+**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`).
+**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+**css_class (optional)** If you want to add additional css classes
+
+**Example**
+
+`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
+
+**Output from example above**
+
+`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+
+### Usage in HTML/JS
+
+Please use the following function inside JS to render an icon :
+`gl.utils.spriteIcon(iconName)`
+
+## Adding a new icon to the sprite
+
+All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
+
+To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
+
+# SVG Illustrations
+
+Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised).
+
+**Example**
+
+`= image_tag 'illustrations/merge_requests.svg'`
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 64bcb4a0257..031b12a8e91 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -29,34 +29,6 @@ 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.
@@ -98,6 +70,11 @@ Vue specific design patterns and practices.
---
+## [Icons](icons.md)
+How we use SVG for our Icons.
+
+---
+
## Style Guides
### [JavaScript Style Guide](style_guide_js.md)
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 4f20aa070de..77ae6d2a0ea 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -311,6 +311,7 @@ A forEach will cause side effects, it will be mutating the array being iterated.
#### Alignment
1. Follow these alignment styles for the template method:
+ 1. With more than one attribute, all attributes should be on a new line:
```javascript
// bad
<component v-if="bar"
@@ -327,9 +328,16 @@ A forEach will cause side effects, it will be mutating the array being iterated.
<button class="btn">
Click me
</button>
+ ```
+ 1. The tag can be inline if there is only one attribute:
+ ```javascript
+ // good
+ <component bar="bar" />
- // if props fit in one line then keep it on the same line
- <component bar="bar" />
+ // good
+ <component
+ bar="bar"
+ />
```
#### Quotes
@@ -381,9 +389,12 @@ A forEach will cause side effects, it will be mutating the array being iterated.
}
```
-1. Default key should always be provided if the prop is not required:
+1. Default key should be provided if the prop is not required.
+_Note:_ There are some scenarios where we need to check for the existence of the property.
+On those a default key should not be provided.
+
```javascript
- // bad
+ // good
props: {
foo: {
type: String,
@@ -459,7 +470,25 @@ A forEach will cause side effects, it will be mutating the array being iterated.
```
#### Ordering
-1. Order for a Vue Component:
+
+1. Tag order in `.vue` file
+
+ ```
+ <script>
+ // ...
+ </script>
+
+ <template>
+ // ...
+ </template>
+
+ // We don't use scoped styles but there are few instances of this
+ <style>
+ // ...
+ </style>
+ ```
+
+1. Properties in a Vue Component:
1. `name`
1. `props`
1. `mixins`
@@ -479,6 +508,7 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. `beforeDestroy`
1. `destroyed`
+
#### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
@@ -512,11 +542,11 @@ A forEach will cause side effects, it will be mutating the array being iterated.
```
### The Javascript/Vue Accord
-The goal of this accord is to make sure we are all on the same page.
+The goal of this accord is to make sure we are all on the same page.
-1. When writing Vue, you may not use jQuery in your application.
+1. When writing Vue, you may not use jQuery in your application.
1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery.
- 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
+ 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
1. We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application.
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 2607353782a..277e0cd5f00 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -428,7 +428,7 @@ is a good example of this pattern.
## Style guide
-Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
+Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js)
for best practices while writing your Vue components and templates.
## Testing Vue Components
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
new file mode 100644
index 00000000000..e41d258bec6
--- /dev/null
+++ b/doc/development/gitaly.md
@@ -0,0 +1,84 @@
+# GitLab Developers Guide to Working with Gitaly
+
+[Gitaly](https://gitlab.com/gitlab-org/gitaly) is a high-level Git RPC service used by GitLab CE/EE,
+Workhorse and GitLab-Shell. All Rugged operations in GitLab CE/EE are currently being phased out to
+be replaced by Gitaly API calls.
+
+Visit the [Gitaly Migration Board](https://gitlab.com/gitlab-org/gitaly/boards/331341) for current
+status of the migration.
+
+## Feature Flags
+
+Gitaly makes heavy use of [feature flags](feature_flags.md).
+
+Each Rugged-to-Gitaly migration goes through a [series of phases](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/MIGRATION_PROCESS.md):
+
+* **Opt-In**: by default the Rugged implementation is used.
+ * Production instances can choose to enable the Gitaly endpoint by enabling the feature flag.
+ * For testing purposes, you may wish to enable all feature flags by default. This can be done by exporting the following
+ environment variable: `GITALY_FEATURE_DEFAULT_ON=1`.
+ * On developer instances (ie, when `Rails.env.development?` is true), the Gitaly endpoint
+ is enabled by default, but can be _disabled_ using feature flags.
+* **Opt-Out**: by default, the Gitaly endpoint is used, but the feature can be explicitly disabled using the feature flag.
+* **Mandatory**: The migration is complete and cannot be disabled. The old codepath is removed.
+
+### Enabling and Disabling Feature
+
+In the Rails console, type:
+
+```ruby
+Feature.enable(:gitaly_feature_name)
+Feature.disable(:gitaly_feature_name)
+```
+
+Where `gitaly_feature_name` is the name of the Gitaly feature. This can be determined by finding the appropriate
+`gitaly_migrate` code block, for example:
+
+```ruby
+gitaly_migrate(:tag_names) do
+...
+end
+```
+
+Since Gitaly features are always prefixed with `gitaly_`, the name of the feature flag in this case would be `gitaly_tag_names`.
+
+## Gitaly-Related Test Failures
+
+If your test-suite is failing with Gitaly issues, as a first step, try running:
+
+```shell
+rm -rf tmp/tests/gitaly
+```
+
+## `TooManyInvocationsError` errors
+
+During development and testing, you may experience `Gitlab::GitalyClient::TooManyInvocationsError` failures.
+The `GitalyClient` will attempt to block against potential n+1 issues by raising this error
+when Gitaly is called more than 30 times in a single Rails request or Sidekiq execution.
+
+As a temporary measure, export `GITALY_DISABLE_REQUEST_LIMITS=1` to suppress the error. This will disable the n+1 detection
+in your development environment.
+
+Please raise an issue in the GitLab CE or EE repositories to report the issue. Include the labels ~Gitaly
+~performance ~"technical debt". Please ensure that the issue contains the full stack trace and error message of the
+`TooManyInvocationsError`. Also include any known failing tests if possible.
+
+Isolate the source of the n+1 problem. This will normally be a loop that results in Gitaly being called for each
+element in an array. If you are unable to isolate the problem, please contact a member
+of the [Gitaly Team](https://gitlab.com/groups/gl-gitaly/group_members) for assistance.
+
+Once the source has been found, wrap it in an `allow_n_plus_1_calls` block, as follows:
+
+```ruby
+# n+1: link to n+1 issue
+Gitlab::GitalyClient.allow_n_plus_1_calls do
+ # original code
+ commits.each { |commit| ... }
+end
+```
+
+Once the code is wrapped in this block, this code-path will be excluded from n+1 detection.
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
new file mode 100644
index 00000000000..167260b6e0e
--- /dev/null
+++ b/doc/development/i18n/externalization.md
@@ -0,0 +1,296 @@
+# Internationalization for GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2.
+
+For working with internationalization (i18n),
+[GNU gettext](https://www.gnu.org/software/gettext/) is used given it's the most
+used tool for this task and there are a lot of applications that will help us to
+work with it.
+
+## Setting up GitLab Development Kit (GDK)
+
+In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce)
+project you must download and configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md).
+
+Once you have the GitLab project ready, you can start working on the translation.
+
+## Tools
+
+The following tools are used:
+
+1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
+ gem allow us to translate content from models, views and controllers. Also
+ it gives us access to the following raketasks:
+ - `rake gettext:find`: Parses almost all the files from the
+ Rails application looking for content that has been marked for
+ translation. Finally, it updates the PO files with the new content that
+ it has found.
+ - `rake gettext:pack`: Processes the PO files and generates the
+ MO files that are binary and are finally used by the application.
+
+1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
+ this gem is useful to make the translations available in JavaScript. It
+ provides the following raketask:
+ - `rake gettext:po_to_json`: Reads the contents from the PO files and
+ generates JSON files containing all the available translations.
+
+1. PO editor: there are multiple applications that can help us to work with PO
+ files, a good option is [Poedit](https://poedit.net/download) which is
+ available for macOS, GNU/Linux and Windows.
+
+## Preparing a page for translation
+
+We basically have 4 types of files:
+
+1. Ruby files: basically Models and Controllers.
+1. HAML files: these are the view files.
+1. ERB files: used for email templates.
+1. JavaScript files: we mostly need to work with VUE JS templates.
+
+### Ruby files
+
+If there is a method or variable that works with a raw string, for instance:
+
+```ruby
+def hello
+ "Hello world!"
+end
+```
+
+Or:
+
+```ruby
+hello = "Hello world!"
+```
+
+You can easily mark that content for translation with:
+
+```ruby
+def hello
+ _("Hello world!")
+end
+```
+
+Or:
+
+```ruby
+hello = _("Hello world!")
+```
+
+### HAML files
+
+Given the following content in HAML:
+
+```haml
+%h1 Hello world!
+```
+
+You can mark that content for translation with:
+
+```haml
+%h1= _("Hello world!")
+```
+
+### ERB files
+
+Given the following content in ERB:
+
+```erb
+<h1>Hello world!</h1>
+```
+
+You can mark that content for translation with:
+
+```erb
+<h1><%= _("Hello world!") %></h1>
+```
+
+### JavaScript files
+
+In JavaScript we added the `__()` (double underscore parenthesis) function
+for translations.
+
+### Updating the PO files with the new content
+
+Now that the new content is marked for translation, we need to update the PO
+files with the following command:
+
+```sh
+bundle exec rake gettext:find
+```
+
+This command will update the `locale/**/gitlab.edit.po` file with the
+new content that the parser has found.
+
+New translations will be added with their default content and will be marked
+fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
+and remove it.
+
+We need to make sure we remove the `fuzzy` translations before generating the
+`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
+be treated as a binary file which could overwrite translations that were merged
+before the new translations.
+
+When we are just preparing a page to be translated, but not actually adding any
+translations. There's no need to generate `.po` files.
+
+Translations that aren't used in the source code anymore will be marked with
+`~#`; these can be removed to keep our translation files clutter-free.
+
+### Validating PO files
+
+To make sure we keep our translation files up to date, there's a linter that is
+running on CI as part of the `static-analysis` job.
+
+To lint the adjustments in PO files locally you can run `rake gettext:lint`.
+
+The linter will take the following into account:
+
+- Valid PO-file syntax
+- Variable usage
+ - Only one unnamed (`%d`) variable, since the order of variables might change
+ in different languages
+ - All variables used in the message-id are used in the translation
+ - There should be no variables used in a translation that aren't in the
+ message-id
+- Errors during translation.
+
+The errors are grouped per file, and per message ID:
+
+```
+Errors in `locale/zh_HK/gitlab.po`:
+ PO-syntax errors
+ SimplePoParser::ParserErrorSyntax error in lines
+ Syntax error in msgctxt
+ Syntax error in msgid
+ Syntax error in msgstr
+ Syntax error in message_line
+ There should be only whitespace until the end of line after the double quote character of a message text.
+ Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
+ SimplePoParser filtered backtrace: SimplePoParser::ParserError
+Errors in `locale/zh_TW/gitlab.po`:
+ 1 pipeline
+ <%d æ¢æµæ°´ç·š> is using unknown variables: [%d]
+ Failure translating to zh_TW with []: too few arguments
+```
+
+In this output the `locale/zh_HK/gitlab.po` has syntax errors.
+The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
+aren't in the message with id `1 pipeline`.
+
+## Working with special content
+
+### Interpolation
+
+- In Ruby/HAML:
+
+ ```ruby
+ _("Hello %{name}") % { name: 'Joe' }
+ ```
+
+- In JavaScript: Not supported at this moment.
+
+### Plurals
+
+- In Ruby/HAML:
+
+ ```ruby
+ n_('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+ ```ruby
+ n_("There is a mouse.", "There are %d mice.", size) % size
+ ```
+
+- In JavaScript:
+
+ ```js
+ n__('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+
+ ```js
+ n__('Last day', 'Last %d days', 30) => 'Last 30 days'
+ ```
+
+### Namespaces
+
+Sometimes you need to add some context to the text that you want to translate
+(if the word occurs in a sentence and/or the word is ambiguous).
+
+- In Ruby/HAML:
+
+ ```ruby
+ s_('OpenedNDaysAgo|Opened')
+ ```
+
+ In case the translation is not found it will return `Opened`.
+
+- In JavaScript:
+
+ ```js
+ s__('OpenedNDaysAgo|Opened')
+ ```
+
+### Just marking content for parsing
+
+Sometimes there are some dynamic translations that can't be found by the
+parser when running `bundle exec rake gettext:find`. For these scenarios you can
+use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+
+There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+
+## Adding a new language
+
+Let's suppose you want to add translations for a new language, let's say French.
+
+1. The first step is to register the new language in `lib/gitlab/i18n.rb`:
+
+ ```ruby
+ ...
+ AVAILABLE_LANGUAGES = {
+ ...,
+ 'fr' => 'Français'
+ }.freeze
+ ...
+ ```
+
+1. Next, you need to add the language:
+
+ ```sh
+ bundle exec rake gettext:add_language[fr]
+ ```
+
+ If you want to add a new language for a specific region, the command is similar,
+ you just need to separate the region with an underscore (`_`). For example:
+
+ ```sh
+ bundle exec rake gettext:add_language[en_GB]
+ ```
+
+ Please note that you need to specify the region part in capitals.
+
+1. Now that the language is added, a new directory has been created under the
+ path: `locale/fr/`. You can now start using your PO editor to edit the PO file
+ located in: `locale/fr/gitlab.edit.po`.
+
+1. After you're done updating the translations, you need to process the PO files
+ in order to generate the binary MO files and finally update the JSON files
+ containing the translations:
+
+ ```sh
+ bundle exec rake gettext:compile
+ ```
+
+1. In order to see the translated content we need to change our preferred language
+ which can be found under the user's **Settings** (`/profile`).
+
+1. After checking that the changes are ok, you can proceed to commit the new files.
+ For example:
+
+ ```sh
+ git add locale/fr/ app/assets/javascripts/locale/fr/
+ git commit -m "Add French translations for Cycle Analytics page"
+ ```
diff --git a/doc/development/i18n/img/crowdin-editor.png b/doc/development/i18n/img/crowdin-editor.png
new file mode 100644
index 00000000000..5c31d8f0cec
--- /dev/null
+++ b/doc/development/i18n/img/crowdin-editor.png
Binary files differ
diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md
new file mode 100644
index 00000000000..4cb2624c098
--- /dev/null
+++ b/doc/development/i18n/index.md
@@ -0,0 +1,76 @@
+# Translate GitLab to your language
+
+The text in GitLab's user interface is in American English by default.
+Each string can be translated to other languages.
+As each string is translated, it is added to the languages translation file,
+and will be available in future releases of GitLab.
+
+Contributions to translations are always needed.
+Many strings are not yet available for translation because they have not been externalized.
+Helping externalize strings benefits all languages.
+Some translations are incomplete or inconsistent.
+Translating strings will help complete and improve each language.
+
+## How to contribute
+
+There are many ways you can contribute in translating GitLab.
+
+### Externalize strings
+
+Before a string can be translated, it must be externalized.
+This is the process where English strings in the GitLab source code are wrapped in a function that
+retrieves the translated string for the user's language.
+
+As new features are added and existing features are updated, the surrounding strings are being
+externalized, however, there are many parts of GitLab that still need more work to externalize all
+strings.
+
+See [Externalization for GitLab](externalization.md).
+
+### Translate strings
+
+The translation process is managed at [translate.gitlab.com](https://translate.gitlab.com)
+using [Crowdin](https://crowdin.com/).
+You will need to create an account before you can submit translations.
+Once you are signed in, select the language you wish to contribute translations to.
+
+Voting for translations is also valuable, helping to confirm good and flag inaccurate translations.
+
+See [Translation guidelines](translation.md).
+
+### Proof reading
+
+Proof reading helps ensure the accuracy and consistency of translations.
+All translations are proof read before being accepted.
+If a translations requires changes, you will be notified with a comment explaining why.
+
+Community assistance proof reading translations is encouraged and appreciated.
+Requests to become a proof reader will be considered on the merits of previous translations.
+
+- Bulgarian
+- Chinese Simplified
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Chinese Traditional
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Chinese Traditional, Hong Kong
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Dutch
+- Esperanto
+- French
+- German
+- Italian
+- Japanese
+- Korean
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Portuguese, Brazilian
+- Russian
+ - [Alexy Lustin](https://crowdin.com/profile/lustin)
+ - [Nikita Grylov](https://crowdin.com/profile/nixel2007)
+- Spanish
+- Ukrainian
+
+If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
+
+## Release
+
+Translations are typically included in the next major or minor release.
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
new file mode 100644
index 00000000000..79707aaf671
--- /dev/null
+++ b/doc/development/i18n/translation.md
@@ -0,0 +1,64 @@
+# Translating GitLab
+
+For managing the translation process we use [Crowdin](https://crowdin.com).
+
+## Using Crowdin
+
+The first step is to get familiar with Crowdin.
+
+### Sign In
+
+To contribute translations at [translate.gitlab.com](https://translate.gitlab.com)
+you must create a Crowdin account.
+You may create a new account or use any of their supported sign in services.
+
+### Language Selections
+
+GitLab is being translated into many languages.
+
+1. Select the language you would like to contribute translations to by clicking the flag
+1. You will see a list of files and folders.
+ Click `gitlab.pot` to open the translation editor.
+
+### Translation Editor
+
+The online translation editor is the easiest way to contribute translations.
+
+![Crowdin Editor](img/crowdin-editor.png)
+
+1. Strings for translation are listed in the left panel
+1. Translations are entered into the central panel.
+ Multiple translations will be required for strings that contains plurals.
+ The string to be translated is shown above with glossary terms highlighted.
+ If the string to be translated is not clear, you can 'Request Context'
+
+A glossary of common terms is available in the right panel by clicking Terms.
+Comments can be added to discuss a translation with the community.
+
+Remember to **Save** each translation.
+
+## Translation Guidelines
+
+Be sure to check the following guidelines before you translate any strings.
+
+### Technical terms
+
+Technical terms should be treated like proper nouns and not be translated.
+This helps maintain a logical connection and consistency between tools (e.g. `git` client) and
+GitLab.
+
+Technical terms that should always be in English are noted in the glossary when using
+[translate.gitlab.com](https://translate.gitlab.com).
+
+### Formality
+
+The level of formality used in software varies by language.
+For example, in French we translate `you` as the informal `tu`.
+
+You can refer to other translated strings and notes in the glossary to assist determining a
+suitable level of formality.
+
+### Updating the glossary
+
+To propose additions to the glossary please
+[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index bd0ef39ca62..f6e949b5fd8 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -1,297 +1 @@
-# Internationalization for GitLab
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2.
-
-For working with internationalization (i18n) we use
-[GNU gettext](https://www.gnu.org/software/gettext/) given it's the most used
-tool for this task and we have a lot of applications that will help us to work
-with it.
-
-## Setting up GitLab Development Kit (GDK)
-
-In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) project we must download and
-configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit), we can do it by following this [guide](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md).
-
-Once we have the GitLab project ready we can start working on the
-translation of the project.
-
-## Tools
-
-We use a couple of gems:
-
-1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
- gem allow us to translate content from models, views and controllers. Also
- it gives us access to the following raketasks:
- - `rake gettext:find`: Parses almost all the files from the
- Rails application looking for content that has been marked for
- translation. Finally, it updates the PO files with the new content that
- it has found.
- - `rake gettext:pack`: Processes the PO files and generates the
- MO files that are binary and are finally used by the application.
-
-1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
- this gem is useful to make the translations available in JavaScript. It
- provides the following raketask:
- - `rake gettext:po_to_json`: Reads the contents from the PO files and
- generates JSON files containing all the available translations.
-
-1. PO editor: there are multiple applications that can help us to work with PO
- files, a good option is [Poedit](https://poedit.net/download) which is
- available for macOS, GNU/Linux and Windows.
-
-## Preparing a page for translation
-
-We basically have 4 types of files:
-
-1. Ruby files: basically Models and Controllers.
-1. HAML files: these are the view files.
-1. ERB files: used for email templates.
-1. JavaScript files: we mostly need to work with VUE JS templates.
-
-### Ruby files
-
-If there is a method or variable that works with a raw string, for instance:
-
-```ruby
-def hello
- "Hello world!"
-end
-```
-
-Or:
-
-```ruby
-hello = "Hello world!"
-```
-
-You can easily mark that content for translation with:
-
-```ruby
-def hello
- _("Hello world!")
-end
-```
-
-Or:
-
-```ruby
-hello = _("Hello world!")
-```
-
-### HAML files
-
-Given the following content in HAML:
-
-```haml
-%h1 Hello world!
-```
-
-You can mark that content for translation with:
-
-```haml
-%h1= _("Hello world!")
-```
-
-### ERB files
-
-Given the following content in ERB:
-
-```erb
-<h1>Hello world!</h1>
-```
-
-You can mark that content for translation with:
-
-```erb
-<h1><%= _("Hello world!") %></h1>
-```
-
-### JavaScript files
-
-In JavaScript we added the `__()` (double underscore parenthesis) function
-for translations.
-
-### Updating the PO files with the new content
-
-Now that the new content is marked for translation, we need to update the PO
-files with the following command:
-
-```sh
-bundle exec rake gettext:find
-```
-
-This command will update the `locale/**/gitlab.edit.po` file with the
-new content that the parser has found.
-
-New translations will be added with their default content and will be marked
-fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
-and remove it.
-
-We need to make sure we remove the `fuzzy` translations before generating the
-`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
-be treated as a binary file which could overwrite translations that were merged
-before the new translations.
-
-When we are just preparing a page to be translated, but not actually adding any
-translations. There's no need to generate `.po` files.
-
-Translations that aren't used in the source code anymore will be marked with
-`~#`; these can be removed to keep our translation files clutter-free.
-
-### Validating PO files
-
-To make sure we keep our translation files up to date, there's a linter that is
-running on CI as part of the `static-analysis` job.
-
-To lint the adjustments in PO files locally you can run `rake gettext:lint`.
-
-The linter will take the following into account:
-
-- Valid PO-file syntax
-- Variable usage
- - Only one unnamed (`%d`) variable, since the order of variables might change
- in different languages
- - All variables used in the message-id are used in the translation
- - There should be no variables used in a translation that aren't in the
- message-id
-- Errors during translation.
-
-The errors are grouped per file, and per message ID:
-
-```
-Errors in `locale/zh_HK/gitlab.po`:
- PO-syntax errors
- SimplePoParser::ParserErrorSyntax error in lines
- Syntax error in msgctxt
- Syntax error in msgid
- Syntax error in msgstr
- Syntax error in message_line
- There should be only whitespace until the end of line after the double quote character of a message text.
- Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
- SimplePoParser filtered backtrace: SimplePoParser::ParserError
-Errors in `locale/zh_TW/gitlab.po`:
- 1 pipeline
- <%d æ¢æµæ°´ç·š> is using unknown variables: [%d]
- Failure translating to zh_TW with []: too few arguments
-```
-
-In this output the `locale/zh_HK/gitlab.po` has syntax errors.
-The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
-aren't in the message with id `1 pipeline`.
-
-## Working with special content
-
-### Interpolation
-
-- In Ruby/HAML:
-
- ```ruby
- _("Hello %{name}") % { name: 'Joe' }
- ```
-
-- In JavaScript: Not supported at this moment.
-
-### Plurals
-
-- In Ruby/HAML:
-
- ```ruby
- n_('Apple', 'Apples', 3) => 'Apples'
- ```
-
- Using interpolation:
- ```ruby
- n_("There is a mouse.", "There are %d mice.", size) % size
- ```
-
-- In JavaScript:
-
- ```js
- n__('Apple', 'Apples', 3) => 'Apples'
- ```
-
- Using interpolation:
-
- ```js
- n__('Last day', 'Last %d days', 30) => 'Last 30 days'
- ```
-
-### Namespaces
-
-Sometimes you need to add some context to the text that you want to translate
-(if the word occurs in a sentence and/or the word is ambiguous).
-
-- In Ruby/HAML:
-
- ```ruby
- s_('OpenedNDaysAgo|Opened')
- ```
-
- In case the translation is not found it will return `Opened`.
-
-- In JavaScript:
-
- ```js
- s__('OpenedNDaysAgo|Opened')
- ```
-
-### Just marking content for parsing
-
-Sometimes there are some dynamic translations that can't be found by the
-parser when running `bundle exec rake gettext:find`. For these scenarios you can
-use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
-
-There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
-
-## Adding a new language
-
-Let's suppose you want to add translations for a new language, let's say French.
-
-1. The first step is to register the new language in `lib/gitlab/i18n.rb`:
-
- ```ruby
- ...
- AVAILABLE_LANGUAGES = {
- ...,
- 'fr' => 'Français'
- }.freeze
- ...
- ```
-
-1. Next, you need to add the language:
-
- ```sh
- bundle exec rake gettext:add_language[fr]
- ```
-
- If you want to add a new language for a specific region, the command is similar,
- you just need to separate the region with an underscore (`_`). For example:
-
- ```sh
- bundle exec rake gettext:add_language[en_GB]
- ```
-
- Please note that you need to specify the region part in capitals.
-
-1. Now that the language is added, a new directory has been created under the
- path: `locale/fr/`. You can now start using your PO editor to edit the PO file
- located in: `locale/fr/gitlab.edit.po`.
-
-1. After you're done updating the translations, you need to process the PO files
- in order to generate the binary MO files and finally update the JSON files
- containing the translations:
-
- ```sh
- bundle exec rake gettext:compile
- ```
-
-1. In order to see the translated content we need to change our preferred language
- which can be found under the user's **Settings** (`/profile`).
-
-1. After checking that the changes are ok, you can proceed to commit the new files.
- For example:
-
- ```sh
- git add locale/fr/ app/assets/javascripts/locale/fr/
- git commit -m "Add French translations for Cycle Analytics page"
- ```
+This document was moved to [a new location](i18n/index.md).
diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png
index fef767c2a79..615facabb5f 100644
--- a/doc/development/img/manual_build_docs.png
+++ b/doc/development/img/manual_build_docs.png
Binary files differ
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 9a5811d8474..a75cdf22f40 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -65,6 +65,7 @@ Libraries with the following licenses are unacceptable for use:
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
- [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation.
+- [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy.
## Requesting Approval for Licenses
@@ -108,3 +109,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[x-list]: https://www.apache.org/legal/resolved.html#category-x
[Acceptable-Licenses]: #acceptable-licenses
[Unacceptable-Licenses]: #unacceptable-licenses
+[WTFPL]: https://wtfpl.net
+[WTFPL-OSI]: https://opensource.org/minutes20090304
diff --git a/doc/development/swapping_tables.md b/doc/development/swapping_tables.md
new file mode 100644
index 00000000000..6b990ece72c
--- /dev/null
+++ b/doc/development/swapping_tables.md
@@ -0,0 +1,53 @@
+# Swapping Tables
+
+Sometimes you need to replace one table with another. For example, when
+migrating data in a very large table it's often better to create a copy of the
+table and insert & migrate the data into this new table in the background.
+
+Let's say you want to swap the table "events" with "events_for_migration". In
+this case you need to follow 3 steps:
+
+1. Rename "events" to "events_temporary"
+2. Rename "events_for_migration" to "events"
+3. Rename "events_temporary" to "events_for_migration"
+
+Rails allows you to do this using the `rename_table` method:
+
+```ruby
+rename_table :events, :events_temporary
+rename_table :events_for_migration, :events
+rename_table :events_temporary, :events_for_migration
+```
+
+This does not require any downtime as long as the 3 `rename_table` calls are
+executed in the _same_ database transaction. Rails by default uses database
+transactions for migrations, but if it doesn't you'll need to start one
+manually:
+
+```ruby
+Event.transaction do
+ rename_table :events, :events_temporary
+ rename_table :events_for_migration, :events
+ rename_table :events_temporary, :events_for_migration
+end
+```
+
+Once swapped you _have to_ reset the primary key of the new table. For
+PostgreSQL you can use the `reset_pk_sequence!` method like so:
+
+```ruby
+reset_pk_sequence!('events')
+```
+
+For MySQL however you need to do run the following:
+
+```ruby
+amount = Event.pluck('COALESCE(MAX(id), 1)').first
+
+execute "ALTER TABLE events AUTO_INCREMENT = #{amount}"
+```
+
+Failure to reset the primary keys will result in newly created rows starting
+with an ID value of 1. Depending on the existing data this can then lead to
+duplicate key constraints from popping up, preventing users from creating new
+data.
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 83269303005..4d5b90de6fc 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -150,6 +150,16 @@ always in-sync with the codebase.
[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
+## Test for what should not be there
+
+This is particularly important for permission calls and might be called a
+negative assertion: make sure only the bare minimum is returned and nothing else.
+
+See an issue about [leaking tokens] as an example of a vulnerability that is
+captured by such a test.
+
+[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948
+
## How to test at the correct level?
As many things in life, deciding what to test at each level of testing is a
@@ -292,7 +302,7 @@ range of inputs, might look like this:
```ruby
describe "#==" do
- using Rspec::Parameterized::TableSyntax
+ using RSpec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
@@ -493,24 +503,24 @@ Here are some things to keep in mind regarding test performance:
Our current CI parallelization setup is as follows:
-1. The `knapsack` job in the prepare stage that is supposed to ensure we have a
- `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
+1. The `retrieve-tests-metadata` job in the `prepare` stage ensures that we have
+ a `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
- The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
from S3, if it's not here we initialize the file with `{}`.
-1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly
- distributed share of tests:
+1. Each `rspec-pg x y`/`rspec-mysql x y` job is run with `knapsack rspec` and
+ should have an evenly distributed share of tests:
- It works because the jobs have access to the
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
from all previous stages are passed by default". [^1]
- - the jobs set their own report path to
+ - The jobs set their own report path to
`KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
- - if knapsack is doing its job, test files that are run should be listed under
+ - If knapsack is doing its job, test files that are run should be listed under
`Report specs`, not under `Leftover specs`.
-1. The `update-knapsack` job takes all the
+1. The `update-tests-metadata` job takes all the
`knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
- files from the `rspec x y` jobs and merge them all together into a single
- `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then
- uploaded to S3.
+ files from the `rspec-pg x y`/`rspec-mysql x y`jobs and merge them all together
+ into a single `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that
+ is then uploaded to S3.
After that, the next pipeline will use the up-to-date
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
index 5dae4bcc905..d190ee1b0ff 100644
--- a/doc/development/ux_guide/animation.md
+++ b/doc/development/ux_guide/animation.md
@@ -39,6 +39,12 @@ When information is updating in place, a quick, subtle animation is needed. The
![Quick update animation](img/animation-quickupdate.gif)
+### Skeleton loading
+
+Skeleton loading is explained in the [component section](components.html#skeleton-loading) of the UX guide. It includes a horizontally pulsating animation that shows motion as if it's growing. It's timing is a slower `linear 1s`.
+
+![Skeleton loading animation](img/skeleton-loading.gif)
+
### Moving transitions
When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`.
@@ -51,7 +57,9 @@ View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here.
![Reorder animation](img/animation-reorder.gif)
#### Autoscroll the page
+
Another example of a moving transition is when you have to autoscroll the page to keep an active element visible.
View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here.
-![Autoscroll animation](img/animation-autoscroll.gif) \ No newline at end of file
+
+![Autoscroll animation](img/animation-autoscroll.gif)
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index a436e9b1948..e215026bcca 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -32,19 +32,17 @@ This is the typeface used for code blocks and references to commits, branches, a
---
## Icons
-GitLab uses Font Awesome icons throughout our interface.
-| | |
-| :-----------: | :---- |
-| ![Trash icon](img/icon-trash.png) | The trash icon is used for destructive actions that deletes information. |
-| ![Edit icon](img/icon-edit.png) | The pencil icon is used for editing content such as comments.|
-| ![Notification icon](img/icon-notification.png) | The bell icon is for notifications, such as Todos. |
-| ![Subscribe icon](img/icon-subscribe.png) | The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label. |
-| ![RSS icon](img/icon-rss.png) | The standard RSS icon is used for linking to RSS/atom feeds. |
-| ![Close icon](img/icon-close.png) | An 'x' is used for closing UI elements such as dropdowns. |
-| ![Add icon](img/icon-add.png) | A plus is used when creating new objects, such as issues, projects, etc. |
-
-> TODO: update this section, add more general guidance to icon usage and personality, etc.
+GitLab has a strong, unique personality. When you look at any screen, you should know immediately that it is GitLab.
+Iconography is a powerful visual cue to the user and is a great way for us to reflect our particular sense of style.
+
+- **Standard size:** 16px * 16px
+- **Border thickness:** 2px
+- **Border radius:** 3px
+
+![Icon sampler](img/icon-spec.png)
+
+> TODO: List all icons, proper usage, hover, and active states.
---
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index ac7c1b6207d..fa31c496b30 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -42,6 +42,37 @@ By default, tooltips should be placed below the referring element. However, if t
---
+## Popovers
+
+Popovers provide additional, useful, unique information about the referring elements and can provide one or multiple actionable elements. They inform the user of additional information within the context of their original view, but without forcing the user to act upon it like a modal. Popovers are different from tooltips, which do not provide rich markup and actionable items. A popover can contain a header section with a different background color.
+
+Popovers are summoned:
+
+* Upon hover or touch on an element
+
+### Usage
+A popover should be used:
+* When you don't want to let the user lose context, but still want to provide additional useful unique information about referring elements
+* When it isn’t critical for the user to act upon the information
+* When you want to give a user a summary of extended information and the option to switch context if they want to dive in deeper.
+
+### Styling
+
+A popover can contain a header section with a different background color if that improves readability and separation of content within.
+
+![Popover usage](img/popover-placement-below.png)
+
+This example shows two sections, where each section includes an actionable element. The first section shows a summary of the content shown when clicking the "read more" link. With this information the user can decide to dive deeper or start their GitLab Enterprise Edition trial immediately.
+
+### Placement
+By default, tooltips should be placed below the referring element. However, if there isn’t enough space in the viewport or it blocks related content, the tooltip should be moved to the side or above as needed.
+
+![Tooltip placement location](img/popover-placement-above.png)
+
+In this example we let the user know more about the setting they are deciding over, without loosing context. If they want to know even more they can do so, but with the expectation of opening that content in a new view.
+
+---
+
## Anchor links
Anchor links are used for navigational actions and lone, secondary commands (such as 'Reset filters' on the Issues List) when deemed appropriate by the UX team.
@@ -204,6 +235,25 @@ Cover blocks are generally used to create a heading element for a page, such as
---
+## Skeleton loading
+
+Skeleton loading is a way to convey to the user what kind of content is currently being loaded. It's a paradigm with which content can independently and asynchronously be loaded, while still adhering to the structure and look of the completely loaded view.
+
+### Requirements
+
+* A skeleton should represent an organism in a recognisable way
+* Atom elements within organisms (for reference see this article on [atomic design methodology](http://atomicdesign.bradfrost.com/chapter-2/)) may be represented in a maximum of 3 repetitions, if applicable.
+* Skeletons should only be presented in grayscale using the HEX colors: `#fafafa` or `#ffffff` (except for shadows)
+* Animate the grey atoms in a pulsating way to show motion, as if "loading". The pulse animation transitions colors horizontally from left to right, starting with `#f2f2f2` to `#fafafa`.
+
+![Skeleton loading animation](img/skeleton-loading.gif)
+
+### Usage
+
+Skeleton loading can replace any existing UI elements for the period in which they are loaded and should aim for maintaining a similar structure visually.
+
+---
+
## Panels
> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
diff --git a/doc/development/ux_guide/illustrations.md b/doc/development/ux_guide/illustrations.md
new file mode 100644
index 00000000000..7e16c300921
--- /dev/null
+++ b/doc/development/ux_guide/illustrations.md
@@ -0,0 +1,86 @@
+# Illustrations
+
+The illustrations should always align with topics and goals in specific context.
+
+## Principles
+
+#### Be simple.
+- For clarity, we use simple and specific elements to create our illustrations.
+
+#### Be optimistic.
+- We are an open-minded, optimistic, and friendly team. We should reflect those values in our illustrations to connect with our brand experience.
+
+#### Be gentle.
+- Our illustrations assist users in understanding context and guide users in the right direction. Illustrations are supportive, so they should be obvious but not aggressive.
+
+
+## Style
+
+#### Shapes
+- All illustrations are geometric rather than organic.
+- The illustrations are made by circles, rectangles, squares, and triangles.
+
+<img src="img/illustrations-geometric.png" width=224px alt="Example for geometric" />
+
+#### Stroke
+- Standard border thickness: **4px**
+- Depending on the situation, border thickness can be changed to **3px**. For example, when the illustration size is small, an illustration with 4px border thickness would look tight. In this case, the border thickness can be changed to 3px.
+- We use **rounded caps** and **rounded corner**.
+
+| Do | Don't |
+| -------- | -------- |
+| <img src="img/illustrations-caps-do.png" width= 133px alt="Do: caps and corner" /> | <img src="img/illustrations-caps-don't.png" width= 133px alt="Don't: caps and corner"/> |
+
+#### Radius
+- Standard corner radius: **10px**
+- Depending on the situation, corner radius can be changed to **5px**. For example, when the illustration size is small, an illustration with 10px corner radius would be over-rounded. In this case, the corner radius can be changed to 5px.
+
+<img src="img/illustrations-border-radius.png" width= 464px alt="Example for border radius"/>
+
+#### Size
+Depends on the situation, the illustration size can be the 3 types below:
+
+**Large**
+* Use case: Empty states, error pages(e.g. 404, 403)
+* For vertical layout, the illustration should not larger than **430*300 px**.
+* For horizontal layout, the illustration should not larger than **430*380 px**.
+
+| Vertical layout | Horizontal layout |
+| --------------- | ----------------- |
+| <img src="img/illustration-size-large-vertical.png" /> | <img src="img/illustration-size-large-horizontal.png" />
+
+**Medium**
+* Use case: Banner
+* The illustration should not larger than **240*160 px**
+* The illustration should keep simple and clear. We recommend not including too many elements in the medium size illustration.
+
+<img src="img/illustration-size-medium.png" width=983px />
+
+**Small**
+* Use case: Graphics for explanatory text, graphics for status.
+* The illustration should not larger than **160*90 px**.
+* The illustration should keep simple and clear. We recommend not including too many elements in the small size illustration.
+
+<img src="img/illustration-size-small.png" width=983px />
+
+**Illustration on mobile**
+- Keep the proportions in original ratio.
+
+#### Colors palette
+
+For consistency, we recommend choosing colors from our color palette.
+
+| Orange | Purple | Grey |
+| -------- | -------- | -------- |
+| <img src="img/illustrations-color-orange.png" width= 160px alt="Orange" /> | <img src="img/illustrations-color-purple.png" width= 160px alt="Purple" /> | <img src="img/illustrations-color-grey.png" width= 160px alt="Grey" /> |
+| #FC6D26 | #6B4FBB | #EEEEEE |
+
+#### Don't
+- Don't include the typography in the illustration.
+- Don't include tanuki in the illustration. If necessary, we recommend having tanuki monochromatic.
+
+---
+
+| Orange | Purple |
+| -------- | -------- |
+| <img src="img/illustrations-palette-oragne.png" width= 160px alt="Palette - Orange" /> | <img src="img/illustrations-palette-purple.png" width= 160px alt="Palette - Purple" /> |
diff --git a/doc/development/ux_guide/img/icon-spec.png b/doc/development/ux_guide/img/icon-spec.png
new file mode 100644
index 00000000000..56b19610dc1
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-spec.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png
new file mode 100644
index 00000000000..8aa835adccc
--- /dev/null
+++ b/doc/development/ux_guide/img/illustration-size-large-horizontal.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-large-vertical.png b/doc/development/ux_guide/img/illustration-size-large-vertical.png
new file mode 100644
index 00000000000..813b6a065e5
--- /dev/null
+++ b/doc/development/ux_guide/img/illustration-size-large-vertical.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png
new file mode 100644
index 00000000000..55cfe1dcb91
--- /dev/null
+++ b/doc/development/ux_guide/img/illustration-size-medium.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-small.png b/doc/development/ux_guide/img/illustration-size-small.png
new file mode 100644
index 00000000000..0124f58f48e
--- /dev/null
+++ b/doc/development/ux_guide/img/illustration-size-small.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png
new file mode 100644
index 00000000000..4e2fef5c7f5
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-border-radius.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png
new file mode 100644
index 00000000000..7a2c74382f6
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-caps-do.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png
new file mode 100644
index 00000000000..848f72dbe30
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-caps-don't.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png
new file mode 100644
index 00000000000..63855026c2b
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-color-grey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png
new file mode 100644
index 00000000000..96765c8c28c
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-color-orange.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png
new file mode 100644
index 00000000000..745d2c853ba
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-color-purple.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png
new file mode 100644
index 00000000000..33f05547bac
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-geometric.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png
new file mode 100644
index 00000000000..15f35912646
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-palette-oragne.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png
new file mode 100644
index 00000000000..e0f5839705e
--- /dev/null
+++ b/doc/development/ux_guide/img/illustrations-palette-purple.png
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-above.png b/doc/development/ux_guide/img/popover-placement-above.png
new file mode 100644
index 00000000000..1aa044bfc9c
--- /dev/null
+++ b/doc/development/ux_guide/img/popover-placement-above.png
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-below.png b/doc/development/ux_guide/img/popover-placement-below.png
new file mode 100644
index 00000000000..2d6ab8a1618
--- /dev/null
+++ b/doc/development/ux_guide/img/popover-placement-below.png
Binary files differ
diff --git a/doc/development/ux_guide/img/skeleton-loading.gif b/doc/development/ux_guide/img/skeleton-loading.gif
new file mode 100644
index 00000000000..5877139171d
--- /dev/null
+++ b/doc/development/ux_guide/img/skeleton-loading.gif
Binary files differ
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
index 8a849f239dc..42bcf234e12 100644
--- a/doc/development/ux_guide/index.md
+++ b/doc/development/ux_guide/index.md
@@ -21,6 +21,11 @@ Guidance on the timing, curving and motion for GitLab.
---
+### [Illustrations](illustrations.md)
+Guidelines for principals and styles related to illustrations for GitLab.
+
+---
+
### [Copy](copy.md)
Conventions on text and messaging within labels, buttons, and other components.
diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md
index cc6d62957e3..ffdeff47d4a 100644
--- a/doc/development/verifying_database_capabilities.md
+++ b/doc/development/verifying_database_capabilities.md
@@ -24,3 +24,15 @@ else
run_query
end
```
+
+# Read-only database
+
+The database can be used in read-only mode. In this case we have to
+make sure all GET requests don't attempt any write operations to the
+database. If one of those requests wants to write to the database, it needs
+to be wrapped in a `Gitlab::Database.read_only?` or `Gitlab::Database.read_write?`
+guard, to make sure it doesn't for read-only databases.
+
+We have a Rails Middleware that filters any potentially writing
+operations (the CUD operations of CRUD) and prevent the user from trying
+to update the database and getting a 500 error (see `Gitlab::Middleware::ReadOnly`).
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 479258f743e..68ba3dd2da3 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -1,6 +1,6 @@
# Writing documentation
- - **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.
+ - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). 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. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
@@ -69,6 +69,51 @@ Use the [writing method](https://about.gitlab.com/handbook/product/technical-wri
All the docs follow the same [styleguide](doc_styleguide.md).
+### Contributing to docs
+
+Whenever a feature is changed, updated, introduced, or deprecated, the merge
+request introducing these changes must be accompanied by the documentation
+(either updating existing ones or creating new ones). This is also valid when
+changes are introduced to the UI.
+
+The one resposible for writing the first piece of documentation is the developer who
+wrote the code. It's the job of the Product Manager to ensure all features are
+shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
+this is the only way to keep the docs up-to-date. If you have any questions about it,
+please ask a Technical Writer. Otherwise, when your content is ready, assign one of
+them to review it for you.
+
+We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
+is documented.
+
+### Feature overview and use cases
+
+Every major feature (regardless if present in GitLab Community or Enterprise editions)
+should present, at the beginning of the document, two main sections: **overview** and
+**use cases**. Every GitLab EE-only feature should also contain these sections.
+
+**Overview**: at the name suggests, the goal here is to provide an overview of the feature.
+Describe what is it, what it does, why it is important/cool/nice-to-have,
+what problem it solves, and what you can do with this feature that you couldn't
+do before.
+
+**Use cases**: provide at least two, ideally three, use cases for every major feature.
+You should answer this question: what can you do with this feature/change? Use cases
+are examples of how this feauture or change can be used in real life.
+
+Examples:
+- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
+- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
+- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
+- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
+
+Note that if you don't have anything to add between the doc title (`<h1>`) and
+the header `## Overview`, you can omit the header, but keep the content of the
+overview there.
+
+> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
+and for every **major** feature present in Community Edition.
+
### 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.
@@ -106,21 +151,84 @@ CE and EE.
## Previewing the changes live
-If you want to preview your changes live, you can use the manual `build-docs`
-job in your merge request.
+If you want to preview the doc changes of your merge request live, you can use
+the manual `review-docs-deploy` job in your merge request.
+
+TIP: **Tip:**
+If your branch contains only documentation changes, you can use
+[special branch names](#testing) to avoid long running pipelines.
![Manual trigger a docs build](img/manual_build_docs.png)
This job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
- project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee`
-1. Trigger a pipeline and build the docs site with your changes
-
-Look for the docs URL at the output of the `build-docs` job.
-
->**Note:**
+ project named after the scheme: `preview-<branch-slug>`
+1. Trigger a cross project pipeline and build the docs site with your changes
+
+After a few minutes, the Review App will be deployed and you will be able to
+preview the changes. The docs URL can be found in two places:
+
+- In the merge request widget
+- In the output of the `review-docs-deploy` job, which also includes the
+ triggered pipeline so that you can investigate whether something went wrong
+
+In case the Review App URL returns 404, follow these steps to debug:
+
+1. **Did you follow the URL from the merge request widget?** If yes, then check if
+ the link is the same as the one in the job output. It can happen that if the
+ branch name slug is longer than 35 characters, it is automatically
+ truncated. That means that the merge request widget will not show the proper
+ URL due to a limitation of how `environment: url` works, but you can find the
+ real URL from the output of the `review-docs-deploy` job.
+1. **Did you follow the URL from the job output?** If yes, then it means that
+ either the site is not yet deployed or something went wrong with the remote
+ pipeline. Give it a few minutes and it should appear online, otherwise you
+ can check the status of the remote pipeline from the link in the job output.
+ If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
+
+TIP: **Tip:**
+Someone that has no merge rights to the CE/EE projects (think of forks from
+contributors) will not be able to run the manual job. In that case, you can
+ask someone from the GitLab team who has the permissions to do that for you.
+
+NOTE: **Note:**
Make sure that you always delete the branch of the merge request you were
working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of
disk space.
+
+### Behind the scenes
+
+If you want to know the hot details, here's what's really happening:
+
+1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
+1. The job runs the [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
+ script with the `deploy` flag, which in turn:
+ 1. Takes your branch name and applies the following:
+ - The slug of the branch name is used to avoid special characters since
+ ultimately this will be used by NGINX.
+ - The `preview-` prefix is added to avoid conflicts if there's a remote branch
+ with the same name that you created in the merge request.
+ - The final branch name is truncated to 42 characters to avoid filesystem
+ limitations with long branch names (> 63 chars).
+ 1. The remote branch is then created if it doesn't exist (meaning you can
+ re-run the manual job as many times as you want and this step will be skipped).
+ 1. A new cross-project pipeline is triggered in the docs project.
+ 1. The preview URL is shown both at the job output and in the merge request
+ widget. You also get the link to the remote pipeline.
+1. In the docs project, the pipeline is created and it
+ [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
+ to lower the build time.
+1. Once the docs site is built, the HTML files are uploaded as artifacts.
+1. A specific Runner tied only to the docs project, runs the Review App job
+ that downloads the artifacts and uses `rsync` to transfer the files over
+ to a location where NGINX serves them.
+
+The following GitLab features are used among others:
+
+- [Manual actions](../ci/yaml/README.md#manual-actions)
+- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
+- [Review Apps](../ci/review_apps/index.md)
+- [Artifacts](../ci/yaml/README.md#artifacts)
+- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index bc75dc1447e..f9ba1508705 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,11 +1,12 @@
# Database MySQL
>**Note:**
-We do not recommend using MySQL due to various issues. For example, case
+- We do not recommend using MySQL due to various issues. For example, case
[(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
and [problems](https://bugs.mysql.com/bug.php?id=65830) that
[suggested](https://bugs.mysql.com/bug.php?id=50909)
[fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
+- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152].
## Initial database setup
@@ -13,7 +14,7 @@ and [problems](https://bugs.mysql.com/bug.php?id=65830) that
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
-# Ensure you have MySQL version 5.5.14 or later
+# Ensure you have MySQL version 5.6 or later
mysql --version
# Pick a MySQL root password (can be anything), type it and press enter
@@ -75,7 +76,7 @@ log_bin_trust_function_creators=1
### MySQL utf8mb4 support
-After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`.
+After installation or upgrade, remember to [convert any new tables](#tables-and-data-conversion-to-utf8mb4) to `utf8mb4`/`utf8mb4_general_ci`.
---
@@ -230,7 +231,6 @@ We need to check, enable and probably convert your existing GitLab DB tables to
> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
#### Tables and data conversion to utf8mb4
-<a name="convert"></a>
Now that you have a persistent MySQL setup, you can safely upgrade tables after setup or upgrade time:
@@ -294,3 +294,4 @@ Details can be found in the [PostgreSQL][postgres-text-type] and
[postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html
[mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
+[ce-38152]: https://gitlab.com/gitlab-org/gitlab-ce/issues/38152
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 66eb7675896..af6c797dc00 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -133,9 +133,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
- echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
- cd ruby-2.3.3
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+ echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+ cd ruby-2.3.5
./configure --disable-install-rdoc
make
sudo make install
@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-5-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab
-**Note:** You can change `9-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index a339bc23809..ddfd47df099 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,9 +1,14 @@
# GitLab Helm Chart
-> **Note:**
-* > **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68).
-* Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **Note**:
+* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). For more information on available charts, please see our [overview](index.md#chart-overview).
+* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+
+
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
+## Introduction
-The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. For most deployments we recommended the [gitlab-omnibus](gitlab_omnibus.md) chart,
+The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment.
This chart includes the following:
@@ -17,7 +22,7 @@ This chart includes the following:
## Prerequisites
-- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required.
+- _At least_ 3 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required.
- Kubernetes 1.4+ with Beta APIs enabled
- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure
- The ability to point a DNS entry or URL at your GitLab install
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index d7fd8613633..8c110a37380 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -2,13 +2,15 @@
> **Note:**
* This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on.
* GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these.
-* Officially supported cloud providers are Google Container Service and Azure Container Service.
+* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
## Introduction
-This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned as well via [Let's Encrypt](https://letsencrypt.org/).
+This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/).
The deployment includes:
@@ -19,7 +21,13 @@ The deployment includes:
- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
- Persistent Volume Claims for Data, Registry, Postgres, and Redis
-A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/).
+### Limitations
+
+* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported.
+* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available.
+* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018.
+
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
## Prerequisites
@@ -46,7 +54,7 @@ Finally, set the `baseIP` setting to this IP address when [deploying GitLab](#co
#### Load Balancer IP
-If you do not specify a `baseIP`, an ephemeral IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab:
+If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab:
`kubectl get svc -w --namespace nginx-ingress nginx`
@@ -140,32 +148,51 @@ helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,g
## Updating GitLab using the Helm Chart
+>**Note**: If you are upgrading from a previous version to 0.1.35 or above, you will need to change the access mode values for GitLab's storage. To do this, set the following in `values.yaml` or on the CLI:
+```
+gitlabDataAccessMode=ReadWriteMany
+gitlabRegistryAccessMode=ReadWriteMany
+gitlabConfigAccessMode=ReadWriteMany
+```
+
Once your GitLab Chart is installed, configuration changes and chart updates
-should we done using `helm upgrade`
+should be done using `helm upgrade`:
+
+```bash
+helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
+```
+
+## Upgrading from CE to EE using the Helm Chart
+
+If you have installed the Community Edition using this chart, upgrading to Enterprise Edition is easy.
+
+If you are using a `values.yaml` file to specify the configuration options, edit the file and set `gitlab=ee`. If you would like to run a specific version of GitLab EE, set `gitlabEEImage` to be the desired GitLab [docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can use `helm upgrade` to update your GitLab instance to EE:
```bash
-helm upgrade -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
+helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
```
-where:
+You can also upgrade and specify these options via the command line:
-- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom
- [configuration] (#configuring-and-installing-gitlab).
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Install section](#installing-gitlab-using-the-helm-chart) we called it `gitlab`.
+```bash
+helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus
+```
## Uninstalling GitLab using the Helm Chart
To uninstall the GitLab Chart, run the following:
```bash
-helm delete <RELEASE-NAME>
+helm delete gitlab
```
-where:
+## Troubleshooting
+
+### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35
+
+Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`.
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Install section](#installing) we called it `gitlab`.
+This is due to a change in the access mode for GitLab storage in version 0.1.35. To successfully upgrade, the access mode flags must be set to `ReadWriteMany` as detailed in the [update section](#updating-gitlab-using-the-helm-chart).
[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index d31c763ed64..5e0d7493b61 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,6 +1,6 @@
# GitLab Runner Helm Chart
> **Note:**
-Officially supported cloud providers are Google Container Service and Azure Container Service.
+These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -11,6 +11,8 @@ This chart configures the Runner to:
- For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a
new pod within the specified namespace to run it.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
## Prerequisites
- Your GitLab Server's API is reachable from the cluster
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index fb6c0c2d263..aed00ae9e2c 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,58 +1,63 @@
# Installing GitLab on Kubernetes
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
-The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is
+The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package
management tool for Kubernetes, allowing apps to be easily managed via their
Charts. A [Chart] is a detailed description of the application including how it
should be deployed, upgraded, and configured.
-GitLab provides [official Helm Charts](#official-gitlab-helm-charts-recommended) which are the recommended way to run GitLab within Kubernetes.
+## Chart Overview
-There are also two other sets of charts:
-* Our [upcoming cloud native Charts](#upcoming-cloud-native-helm-charts), which are in development but will eventually replace the current official charts.
-* [Community contributed charts](#community-contributed-helm-charts). These charts should be considered deprecated, in favor of the official charts.
+* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added.
+* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
+* Other Charts
+ * [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
+ * [Advanced GitLab Installation](gitlab_chart.md): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options.
+ * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
-## Official GitLab Helm Charts
+## GitLab-Omnibus Chart (Recommended)
+> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
-These charts utilize our [GitLab Omnibus Docker images](https://docs.gitlab.com/omnibus/docker/README.html). You can report any issues and feedback related to these charts at
-https://gitlab.com/charts/charts.gitlab.io/issues.
+This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
-### Deploying GitLab on Kubernetes
-> **Note**: This chart will eventually be replaced by the [cloud native charts](#upcoming-cloud-native-helm-charts), which are presently in development.
+Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
-The best way to deploy GitLab on Kubernetes is to use the [gitlab-omnibus](gitlab_omnibus.md) chart.
+Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md)
-It includes everything needed to run GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and an [Ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being completed.
+## Cloud Native GitLab Chart
-### Deploying just the GitLab Runner
+GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
-To deploy just the [GitLab Runner](https://docs.gitlab.com/runner/), utilize the [gitlab-runner](gitlab_runner_chart.md) chart.
+By offering individual containers and charts, we will be able to provide a number of benefits:
+* Easier horizontal scaling of each service,
+* Smaller, more efficient images,
+* Potential for rolling updates and canaries within a service,
+* and plenty more.
-It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running.
+This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018.
-### Advanced deployment of GitLab
-> **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68).
+Learn more about the [cloud native GitLab chart.](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)
-If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the GitLab service along with optional Postgres and Redis. It offers extensive configuration, but requires deep knowledge of Kubernetes and Helm to use.
+## Other Charts
-For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart.
+### GitLab Runner Chart
-## Upcoming Cloud Native Helm Charts
+If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart.
-GitLab is working towards a building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into it's [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
+Learn more about [gitlab-runner chart.](gitlab_runner_chart.md)
-By offering individual containers and charts, we will be able to provide a number of benefits:
-* Easier horizontal scaling of each service
-* Smaller more efficient images
-* Potential for rolling updates and canaries within a service
-* and plenty more.
+### Advanced GitLab Installation
+
+If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use.
+
+This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA.
-This is a large project and will be worked on over the span of multiple releases. For the most up to date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420).
+Learn more about the [gitlab chart.](gitlab_chart.md)
-## Community Contributed Helm Charts
+### Community Contributed Charts
-The community has also [contributed GitLab charts](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts should be considered [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Charts](#official-gitlab-helm-charts-recommended).
+The community has also contributed GitLab [CE](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) and [EE](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ee) charts to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts should be considered [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Charts](gitlab_omnibus.md).
[chart]: https://github.com/kubernetes/charts
[helm]: https://github.com/kubernetes/helm/blob/master/README.md
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index f672b358096..17fe80fa93d 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -82,11 +82,11 @@ errors during usage.
We recommend having at least 2GB of swap on your server, even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
-if your available memory changes. We also recommend [configuring the kernels swappiness setting](https://askubuntu.com/a/103916)
+if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916)
to a low value like `10` to make the most of your RAM while still having the swap
available when needed.
-Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
+Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 5e3e9f5ab77..f3c9c498634 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -74,6 +74,9 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap
tenant_id: "TENANT ID" } }
```
+ The `base_azure_url` is optional and can be added for different locales;
+ e.g. `base_azure_url: "https://login.microsoftonline.de"`.
+
1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above.
1. Save the configuration file.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index ae69d7f92f2..e4c09b2b507 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -370,7 +370,7 @@ This is recommended to reduce cron spam.
## Restore
-GitLab provides a simple command line interface to backup your whole installation,
+GitLab provides a simple command line interface to restore your whole installation,
and is flexible enough to fit your needs.
The [restore prerequisites section](#restore-prerequisites) includes crucial
@@ -445,6 +445,14 @@ Restoring repositories:
Deleting tmp directories...[DONE]
```
+Next, restore `/home/git/gitlab/.secret` if necessary as mentioned above.
+
+Restart GitLab:
+
+```shell
+sudo service gitlab restart
+```
+
### Restore for Omnibus installations
This procedure assumes that:
@@ -480,10 +488,12 @@ restore:
sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
```
+Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
+
Restart and check GitLab:
```shell
-sudo gitlab-ctl start
+sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true
```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index fac91935a45..597c98fbf6b 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
- **Integrations:**
- [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth)
+ - [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md)
## GitLab administrators
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
deleted file mode 100644
index 57bd7650a30..00000000000
--- a/doc/topics/autodevops/img/auto_devops_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
index 5661b50841b..92902e3ca72 100644
--- a/doc/topics/autodevops/img/auto_monitoring.png
+++ b/doc/topics/autodevops/img/auto_monitoring.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index babf44d2665..5561784ed0b 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -1,73 +1,51 @@
# Auto DevOps
-> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and
-**not recommended for production use**. 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** even for public
-projects. On-going secure access is planned for a subsequent release.
-
-Auto DevOps brings best practices to your project in an easy and default way. A
-typical web project starts with Continuous Integration (CI), then adds automated
-deployment to production, and maybe some time in the future adds some kind of
-monitoring. With Auto DevOps, every project has a complete workflow, with
-no configuration, including:
-
-- [Auto Build](#auto-build)
-- [Auto Test](#auto-test)
-- [Auto Code Quality](#auto-code-quality)
-- [Auto Review Apps](#auto-review-apps)
-- [Auto Deploy](#auto-deploy)
-- [Auto Monitoring](#-auto-monitoring)
+DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
+
+> [Introduced][ce-37115] in GitLab 10.0.
+
+Auto DevOps automatically detects, builds, tests, deploys, and monitors your
+applications.
## Overview
-You will need [Kubernetes](https://kubernetes.io/) and
-[Prometheus](https://prometheus.io/) to make full use of Auto DevOps, but
-even projects using only [GitLab Runners](https://docs.gitlab.com/runner/) will
-be able to make use of Auto Build, Auto Test, and Auto Code Quality.
-
-Auto DevOps makes use of an open source tool called
-[Herokuish](https://github.com/gliderlabs/herokuish) which uses [Heroku
-buildpacks](https://devcenter.heroku.com/articles/buildpacks) to automatically
-detect, build, and test applications. Auto DevOps supports all of the languages
-and frameworks that are [supported by
-Herokuish](https://github.com/gliderlabs/herokuish#buildpacks) such as Ruby,
-Rails, Node, PHP, Python, and Java, and [custom buildpacks can be
-specified](#using-custom-buildpacks). *GitLab is in no way affiliated with Heroku
-or Glider Labs.*
-
-Projects can [customize](#customizing) the process by specifying [custom
-buildpacks](#custom-buildpack), [custom `Dockerfile`s](#custom-dockerfile),
-[custom Helm charts](#custom-helm-chart), or even copying the complete CI/CD
-configuration into your project to enable staging and canary deployments, and
-more.
+With Auto DevOps, the software development process becomes easier to set up
+as every project can have a complete workflow from build to deploy and monitoring,
+with minimal to zero configuration.
-## Quick start
+Comprised of a set of stages, Auto DevOps brings these best practices to your
+project in an easy and automatic way:
-If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
-for using Auto DevOps with GitLab.com and an external Kubernetes cluster on
-Google Cloud.
+1. [Auto Build](#auto-build)
+1. [Auto Test](#auto-test)
+1. [Auto Code Quality](#auto-code-quality)
+1. [Auto Review Apps](#auto-review-apps)
+1. [Auto Deploy](#auto-deploy)
+1. [Auto Monitoring](#auto-monitoring)
-For self-hosted installations, the easiest way to make use of Auto DevOps is to
-install GitLab inside a Kubernetes cluster using the [GitLab-Omnibus Helm
-Chart](../../install/kubernetes/gitlab_omnibus.md) which automatically installs
-and configures everything you need.
+As Auto DevOps relies on many different components, it's good to have a basic
+knowledge of the following:
-## Prerequisites
+- [Kubernetes](https://kubernetes.io/docs/home/)
+- [Helm](https://docs.helm.sh/)
+- [Docker](https://docs.docker.com)
+- [GitLab Runner](https://docs.gitlab.com/runner/)
+- [Prometheus](https://prometheus.io/docs/introduction/overview/)
-You will need one or more GitLab Runners, a Kubernetes cluster, and Prometheus
-installed in the cluster to make full use of Auto DevOps. If you do not have
-Kubernetes or Prometheus installed then Auto Review Apps, Auto Deploy, and Auto
-Monitoring will be silently skipped.
+Auto DevOps provides great defaults for all the stages; you can, however,
+[customize](#customizing) almost everything to your needs.
+
+## Prerequisites
-If you are using GitLab outside of Kubernetes, for example with GitLab.com, then
-you should take these prerequisites into account:
+TIP: **Tip:**
+For self-hosted installations, the easiest way to make use of Auto DevOps is to
+install GitLab inside a Kubernetes cluster using the [GitLab Omnibus Helm Chart]
+which automatically installs and configures everything you need!
-1. **Base domain** - You will need a base domain configured with wildcard DNS to
- be used by all of your Auto DevOps applications.
+To make full use of Auto DevOps, you will need:
-1. **GitLab Runner** - Your Runner needs to be configured to be able to run Docker.
- Generally this means using the
+1. **GitLab Runner** (needed for all stages) - Your Runner needs to be
+ configured to be able to run Docker. Generally this means using the
[Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes
executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with
[privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
@@ -78,35 +56,98 @@ you should take these prerequisites into account:
should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner)
for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner)
that are assigned to specific projects.
-
-1. **Kubernetes** - To enable deploys, you will need Kubernetes 1.5+, with NGINX
- ingress and wildcard SSL termination, for example using the
- [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
- and [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego)
- Helm charts respectively. The [Kubernetes service][kubernetes-service]
+1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need
+ a domain configured with wildcard DNS which is gonna be used by all of your
+ Auto DevOps applications. [Read the specifics](#auto-devops-base-domain).
+1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) -
+ To enable deployments, you will need Kubernetes 1.5+. The [Kubernetes service][kubernetes-service]
integration will need to be enabled for the project, or enabled as a
[default service template](../../user/project/integrations/services_templates.md)
for the entire GitLab installation.
-
-1. **Prometheus** - To enable Auto Monitoring, you will need Prometheus installed
- somewhere (inside or outside your cluster) and configured to scrape your
- Kubernetes cluster. To get response metrics (in addition to system metrics),
- you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics).
+ 1. **A load balancer** - You can use NGINX ingress by deploying it to your
+ Kubernetes cluster using the
+ [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
+ Helm chart.
+ 1. **Wildcard TLS termination** - You can deploy the
+ [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego)
+ Helm chart to your Kubernetes cluster to automatically issue certificates
+ for your domains using Let's Encrypt.
+1. **Prometheus** (needed for Auto Monitoring) - To enable Auto Monitoring, you
+ will need Prometheus installed somewhere (inside or outside your cluster) and
+ configured to scrape your Kubernetes cluster. To get response metrics
+ (in addition to system metrics), you need to
+ [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics).
The [Prometheus service](../../user/project/integrations/prometheus.md)
integration needs to be enabled for the project, or enabled as a
[default service template](../../user/project/integrations/services_templates.md)
for the entire GitLab installation.
-## Enabling Auto DevOps
+NOTE: **Note:**
+If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
+Auto Deploy, and Auto Monitoring will be silently skipped.
+
+### Auto DevOps base domain
+
+The Auto DevOps base domain is required if you want to make use of [Auto
+Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
+under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops).
+It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
+
+A wildcard DNS A record matching the base domain is required, for example,
+given a base domain of `example.com`, you'd need a DNS entry like:
+
+```
+*.example.com 3600 A 1.2.3.4
+```
-In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
-section. Select "Enable Auto DevOps", add in your base domain, and save.
+where `example.com` is the domain name under which the deployed apps will be served,
+and `1.2.3.4` is the IP address of your load balancer; generally NGINX
+([see prerequisites](#prerequisites)). How to set up the DNS record is beyond
+the scope of this document; you should check with your DNS provider.
-![auto devops settings](img/auto_devops_settings.png)
+Once set up, all requests will hit the load balancer, which in turn will route
+them to the Kubernetes pods that run your application(s).
+
+NOTE: **Note:**
+If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two
+options: provide a static IP, or have one assigned. For more information see the
+relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites).
+
+## Quick start
+
+If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
+for using Auto DevOps with GitLab.com and an external Kubernetes cluster on
+Google Cloud.
+
+## Enabling Auto DevOps
+
+NOTE: **Note:**
+If you haven't done already, read the [prerequisites](#prerequisites) to make
+full use of Auto DevOps. If this is your fist time, we recommend you follow the
+[quick start guide](#quick-start).
+
+1. Go to your project's **Settings > CI/CD > General pipelines settings** and
+ find the Auto DevOps section
+1. Select "Enable Auto DevOps"
+1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
+ that will be used by Kubernetes to deploy your application
+1. Hit **Save changes** for the changes to take effect
+
+Now that it's enabled, there are a few more steps depending on whether your project
+has a `.gitlab-ci.yml` or not:
+
+- **For projects with no `.gitlab-ci.yml` present:**
+ A pipeline needs to be triggered either by pushing a new commit to the
+ repository or manually visiting `https://example.gitlab.com/<username>/<project>/pipelines/new`
+ and creating a new pipeline for your default branch, generally `master`.
+- **For projects with a `.gitlab-ci.yml` present:**
+ All you need to do is remove your existing `.gitlab-ci.yml`, and you can even
+ do that in a branch to test Auto DevOps before committing to `master`.
## Stages of Auto DevOps
-The following sections describe the stages of Auto DevOps.
+The following sections describe the stages of Auto DevOps. Read them carefully
+to understand how each one works.
### Auto Build
@@ -118,18 +159,24 @@ Auto Build creates a build of the application in one of two ways:
to automatically detect and build the application into a Docker image.
Either way, the resulting Docker image is automatically pushed to the
-[Container Registry][container-registry], tagged with the commit SHA.
+[Container Registry][container-registry] and tagged with the commit SHA.
+
+CAUTION: **Important:**
+If you are also using Auto Review Apps and Auto Deploy and choose to provide
+your own `Dockerfile`, make sure you expose your application to port
+`5000` as this is the port assumed by the default Helm chart.
### Auto Test
-Auto Test automatically tests your application using
+Auto Test automatically runs the appropriate tests for your application using
[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku
-buildpacks](https://devcenter.heroku.com/articles/buildpacks). Auto Test will
-analyze your project to detect the language and framework, and run appropriate
-tests. Several languages and frameworks are detected automatically, but if your
-language is not detected, you may succeed with a [custom
-buildpack](#custom-buildpack).
+buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing
+your project to detect the language and framework. Several languages and
+frameworks are detected automatically, but if your language is not detected,
+you may succeed with a [custom buildpack](#custom-buildpacks). Check the
+[currently supported languages](#currently-supported-languages).
+NOTE: **Note:**
Auto Test uses tests you already have in your application. If there are no
tests, it's up to you to add them.
@@ -137,99 +184,110 @@ tests, it's up to you to add them.
Auto Code Quality uses the open source
[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run
-static analysis and other code checks on the current code, creating a report
-that is uploaded as an artifact. In GitLab EE, differences between the source
-and target branches are shown in the merge request widget. *GitLab is in no way
-affiliated with Code Climate.*
+static analysis and other code checks on the current code. The report is
+created, and is uploaded as an artifact which you can later download and check
+out. In GitLab Enterprise Edition Starter, differences between the source and
+target branches are
+[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
### Auto Review Apps
-Auto Review Apps create a [Review App][review-app] for each branch. Review Apps
-are temporary application environments based on the branch's code so developers,
-designers, QA, product managers, and other reviewers can actually see and
-interact with code changes as part of the review process.
-
-The review app will have a unique URL based on the project name, the branch
+NOTE: **Note:**
+This is an optional step, since many projects do not have a Kubernetes cluster
+available. If the [prerequisites](#prerequisites) are not met, the job will
+silently be skipped.
+
+CAUTION: **Caution:**
+Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.)
+This can cause confusion with Helm not detecting the change, and subsequent
+deploys with Auto DevOps can undo your changes. Also, if you change something
+and want to undo it by deploying again, Helm may not detect that anything changed
+in the first place, and thus not realize that it needs to re-apply the old config.
+
+[Review Apps][review-app] are temporary application environments based on the
+branch's code so developers, designers, QA, product managers, and other
+reviewers can actually see and interact with code changes as part of the review
+process. Auto Review Apps create a Review App for each branch.
+
+The Review App will have a unique URL based on the project name, the branch
name, and a unique number, combined with the Auto DevOps base domain. For
example, `user-project-branch-1234.example.com`. A link to the Review App shows
up in the merge request widget for easy discovery. When the branch is deleted,
for example after the merge request is merged, the Review App will automatically
be deleted.
-This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the Kubernetes service is not configured, or if the variable
-`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto
-DevOps setting), the job will silently be skipped.
-
### Auto Deploy
-After a branch or merge request is merged into `master`, Auto Deploy deploys the
-application to a `production` environment in the Kubernetes cluster, with a
-namespace based on the project name and unique project ID. e.g. `project-4321`.
+NOTE: **Note:**
This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the Kubernetes service is not configured, or if the variable
-`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto
-DevOps setting), the job will silently be skipped.
+available. If the [prerequisites](#prerequisites) are not met, the job will
+silently be skipped.
+
+CAUTION: **Caution:**
+Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.)
+This can cause confusion with Helm not detecting the change, and subsequent
+deploys with Auto DevOps can undo your changes. Also, if you change something
+and want to undo it by deploying again, Helm may not detect that anything changed
+in the first place, and thus not realize that it needs to re-apply the old config.
+
+After a branch or merge request is merged into the project's default branch (usually
+`master`), Auto Deploy deploys the application to a `production` environment in
+the Kubernetes cluster, with a namespace based on the project name and unique
+project ID, for example `project-4321`.
Auto Deploy doesn't include deployments to staging or canary by default, but the
-Auto DevOps template contains job definitions for these tasks if you want to
+[Auto DevOps template] contains job definitions for these tasks if you want to
enable them.
+You can make use of [environment variables](#helm-chart-variables) to automatically
+scale your pod replicas.
+
### Auto Monitoring
+NOTE: **Note:**
+Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage
+work.
+
Once your application is deployed, Auto Monitoring makes it possible to monitor
your application's server and response metrics right out of the box. Auto
Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to
get system metrics such as CPU and memory usage directly from
[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md),
and response metrics such as HTTP error rates, latency, and throughput from the
-[NGINX
-server](../../user/project/integrations/prometheus_library/nginx_ingress.md).
+[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md).
-* Response Metrics: latency, throughput, error rate
-* System Metrics: CPU utilization, memory utilization
+The metrics include:
-To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments).
+- **Response Metrics:** latency, throughput, error rate
+- **System Metrics:** CPU utilization, memory utilization
-![Auto Metrics](img/auto_monitoring.png)
-
-### Configuring Auto Monitoring
-
-If GitLab has been deployed using the
-[omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no
+If GitLab has been deployed using the [GitLab Omnibus Helm Chart], no
configuration is required.
-If you have installed GitLab using a different method:
+If you have installed GitLab using a different method, you need to:
1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
-1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
-1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
-
-## Customizing
+1. If you would like response metrics, ensure you are running at least version
+ 0.9.0 of NGINX Ingress and
+ [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
+1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
+ the NGINX Ingress deployment to be scraped by Prometheus using
+ `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
-### PostgreSQL Database Support
+To view the metrics, open the
+[Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments).
-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`.
-
-PostgreSQL provisioning can be disabled by creating a project variable
-`POSTGRES_ENABLED` set to `false`.
-
-#### PostgreSQL Variables
+![Auto Metrics](img/auto_monitoring.png)
-Any variables set at the project or group level will override variables set in
-the CI/CD configuration.
+## Customizing
-1. `POSTGRES_ENABLED: "false"`: 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
+While Auto DevOps provides great defaults to get you started, you can customize
+almost everything to fit your needs; from custom [buildpacks](#custom-buildpacks),
+to [`Dockerfile`s](#custom-dockerfile), [Helm charts](#custom-helm-chart), or
+even copying the complete [CI/CD configuration](#customizing-gitlab-ci-yml)
+into your project to enable staging and canary deployments, and more.
-### Custom buildpack
+### Custom buildpacks
If the automatic buildpack detection fails for your project, or if you want to
use a custom buildpack, you can override the buildpack using a project variable
@@ -237,12 +295,12 @@ or a `.buildpack` file in your project:
- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL
of the buildpack to use.
-
- **`.buildpack` file** - Add a file in your project's repo called `.buildpack`
and add the URL of the buildpack to use on a line in the file. If you want to
- use multiple buildpacks, you can enter them in, one on each line
+ use multiple buildpacks, you can enter them in, one on each line.
- >**Note:** Using multiple buildpacks may break Auto Test.
+CAUTION: **Caution:**
+Using multiple buildpacks isn't yet supported by Auto DevOps.
### Custom `Dockerfile`
@@ -253,37 +311,138 @@ Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/).
### Custom Helm Chart
-Auto DevOps uses Helm to deploy your application to Kubernetes. You can override
-the Helm chart used by bundling up a chart into your project repo or by
-specifying a project variable.
-
-**Bundled chart** - If your project has a `chart` directory with a `Chart.yaml`
-file in it, Auto DevOps will detect the chart and use it instead of the default
-chart. This can be a great way to control exactly how your application is
-deployed.
+Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernetes.
+You can override the Helm chart used by bundling up a chart into your project
+repo or by specifying a project variable:
-**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the
-URL of a custom chart to use.
+- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml`
+ file in it, Auto DevOps will detect the chart and use it instead of the [default
+ one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
+ This can be a great way to control exactly how your application is deployed.
+- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables)
+ `AUTO_DEVOPS_CHART` with the URL of a custom chart to use.
-### Enable staging, canaries, and more with custom `.gitlab-ci.yml`
+### Customizing `.gitlab-ci.yml`
If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the
-Auto DevOps template into your project's repo and edit as you see fit.
+[Auto DevOps template] into your project's repo and edit as you see fit.
-From your project home page, click on the `Set up CI` button, or click on the `+`
-button and `New file` and pick `.gitlab-ci.yml` as the template type, or view an
-existing `.gitlab-ci.yml` file. Then select "Auto DevOps" from the template
-dropdown. You will then be able to edit or add any jobs needed.
+Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file
+present:
-For example, if you want deploys to go to a staging environment instead of
-directly to a production environment, you can enable the `staging` job by
+1. From your project home page, either click on the "Set up CI" button, or click
+ on the plus button and (`+`), then "New file"
+1. Pick `.gitlab-ci.yml` as the template type
+1. Select "Auto-DevOps" from the template dropdown
+1. Edit the template or add any jobs needed
+1. Give an appropriate commit message and hit "Commit changes"
+
+TIP: **Tip:** The Auto DevOps template includes useful comments to help you
+customize it. For example, if you want deployments to go to a staging environment
+instead of directly to a production one, you can enable the `staging` job by
renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of
the `production` job to turn it into a manual action instead of deploying
automatically.
+### PostgreSQL database support
+
+In order to support applications that require a database,
+[PostgreSQL][postgresql] is provisioned by default. The credentials to access
+the database are preconfigured, but can be customized by setting the associated
+[variables](#environment-variables). These credentials can be used for defining a
+`DATABASE_URL` of the format:
+
+```yaml
+postgres://user:password@postgres-host:postgres-port/postgres-database
+```
+
+### Environment variables
+
+The following variables can be used for setting up the Auto DevOps domain,
+providing a custom Helm chart, or scaling your application. PostgreSQL can be
+also be customized, and you can easily use a [custom buildpack](#custom-buildpacks).
+
+| **Variable** | **Description** |
+| ------------ | --------------- |
+| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). |
+| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). |
+| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment; defaults to 1. |
+| `CANARY_PRODUCTION_REPLICAS`| The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. |
+| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
+| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. |
+| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
+| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
+| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142`|
+
+TIP: **Tip:**
+Set up the replica variables using a
+[project variable](../../ci/variables/README.md#secret-variables)
+and scale your application by just redeploying it!
+
+CAUTION: **Caution:**
+You should *not* scale your application using Kubernetes directly. This can
+cause confusion with Helm not detecting the change, and subsequent deploys with
+Auto DevOps can undo your changes.
+
+#### Advanced replica variables setup
+
+Apart from the two replica-related variables for production mentioned above,
+you can also use others for different environments.
+
+There's a very specific mapping between Kubernetes' label named `track`,
+GitLab CI/CD environment names, and the replicas environment variable.
+The general rule is: `TRACK_ENV_REPLICAS`. Where:
+
+- `TRACK`: The capitalized value of the `track`
+ [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
+ in the Helm Chart app definition. If not set, it will not be taken into account
+ to the variable name.
+- `ENV`: The capitalized environment name of the deploy job that is set in
+ `.gitlab-ci.yml`.
+
+That way, you can define your own `TRACK_ENV_REPLICAS` variables with which
+you will be able to scale the pod's replicas easily.
+
+In the example below, the environment's name is `qa` which would result in
+looking for the `QA_REPLICAS` environment variable:
+
+```yaml
+QA testing:
+ stage: deploy
+ environment:
+ name: qa
+ script:
+ - deploy qa
+```
+
+If, in addition, there was also a `track: foo` defined in the application's Helm
+chart, like:
+
+```yaml
+replicaCount: 1
+image:
+ repository: gitlab.example.com/group/project
+ tag: stable
+ pullPolicy: Always
+ secrets:
+ - name: gitlab-registry
+application:
+ track: foo
+ tier: web
+service:
+ enabled: true
+ name: web
+ type: ClusterIP
+ url: http://my.host.com/
+ externalPort: 5000
+ internalPort: 5000
+```
+
+then the environment variable would be `FOO_QA_REPLICAS`.
+
## Currently supported languages
->**Note:**
+NOTE: **Note:**
Not all buildpacks support Auto Test yet, as it's a relatively new
enhancement. All of Heroku's [officially supported
languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages)
@@ -294,22 +453,28 @@ multi-buildpack does not.
As of GitLab 10.0, the supported buildpacks are:
```
-* heroku-buildpack-multi v1.0.0
-* heroku-buildpack-ruby v168
-* heroku-buildpack-nodejs v99
-* heroku-buildpack-clojure v77
-* heroku-buildpack-python v99
-* heroku-buildpack-java v53
-* heroku-buildpack-gradle v23
-* heroku-buildpack-scala v78
-* heroku-buildpack-play v26
-* heroku-buildpack-php v122
-* heroku-buildpack-go v72
-* heroku-buildpack-erlang fa17af9
-* buildpack-nginx v8
+- heroku-buildpack-multi v1.0.0
+- heroku-buildpack-ruby v168
+- heroku-buildpack-nodejs v99
+- heroku-buildpack-clojure v77
+- heroku-buildpack-python v99
+- heroku-buildpack-java v53
+- heroku-buildpack-gradle v23
+- heroku-buildpack-scala v78
+- heroku-buildpack-play v26
+- heroku-buildpack-php v122
+- heroku-buildpack-go v72
+- heroku-buildpack-erlang fa17af9
+- buildpack-nginx v8
```
-## Private Project Support - Experimental
+## Limitations
+
+The following restrictions apply.
+
+### Private project support
+
+CAUTION: **Caution:** Private project support in Auto DevOps is experimental.
When a project has been marked as private, GitLab's [Container
Registry][container-registry] requires authentication when downloading
@@ -319,8 +484,8 @@ 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
+Container Registry. **Restarting a pod, scaling a service, or other actions which
+require on-going access to the registry may fail**. On-going secure access is
planned for a subsequent release.
## Troubleshooting
@@ -330,13 +495,36 @@ planned for a subsequent release.
key files the buildpack is looking for. For example, for ruby apps, you must
have a `Gemfile` to be properly detected, even though it is possible to write a
Ruby app without a `Gemfile`. Try specifying a [custom
- buildpack](#custom-buildpack).
+ buildpack](#custom-buildpacks).
- Auto Test may fail because of a mismatch between testing frameworks. In this
case, you may need to customize your `.gitlab-ci.yml` with your test commands.
+### Disable the banner instance wide
+
+If an administrator would like to disable the banners on an instance level, this
+feature can be disabled either through the console:
+
+```sh
+sudo gitlab-rails console
+```
+
+Then run:
+
+```ruby
+Feature.get(:auto_devops_banner_disabled).enable
+```
+
+Or through the HTTP API with an admin access token:
+
+```sh
+curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
+```
+
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../../ci/review_apps/index.md
[container-registry]: ../../user/project/container_registry.md
[postgresql]: https://www.postgresql.org/
+[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml
+[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index f23c9d794b4..ffe05519d7b 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -1,14 +1,15 @@
# Auto DevOps: quick start guide
-> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and
-**not recommended for production use**.
+DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
+
+> [Introduced][ce-37115] in GitLab 10.0.
This is a step-by-step guide to deploying a project hosted on GitLab.com to
Google Cloud, using Auto DevOps.
We made a minimal [Ruby
-application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an
-example for this guide. It contains two files:
+application](https://gitlab.com/auto-devops-examples/minimal-ruby-app) to use
+as an example for this guide. It contains two main files:
* `server.rb` - our application. It will start an HTTP server on port 5000 and
render "Hello, world!"
@@ -18,45 +19,66 @@ example for this guide. It contains two files:
## Fork sample project on GitLab.com
Let’s start by forking our sample application. Go to [the project
-page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork`
-button. Soon you should have a project under your namespace with the necessary
-files.
+page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
+**Fork** button. Soon you should have a project under your namespace with the
+necessary files.
## Setup your own cluster on Google Container Engine
-If you do not already have a Google Cloud account, create one at https://console.cloud.google.com.
+If you do not already have a Google Cloud account, create one at
+https://console.cloud.google.com.
-Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
+Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list)
+tab and create a new cluster. You can change the name and leave the rest of the
+default settings. Once you have your cluster running, you need to connect to the
+cluster by following the Google interface.
## Connect to Kubernetes cluster
You need to have the Google Cloud SDK installed. e.g.
-On OSX, install [homebrew](https://brew.sh):
+On macOS, install [homebrew](https://brew.sh):
1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
-3. Add `kubectl`: `gcloud components install kubectl`
+3. Add `kubectl` with: `gcloud components install kubectl`
4. Log in: `gcloud auth login`
-Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`.
+Now go back to the Google interface, find your cluster, follow the instructions
+under "Connect to the cluster" and open the Kubernetes Dashboard. It will look
+something like:
+
+```sh
+gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX
+```
+
+Finally, run `kubectl proxy`.
![connect to cluster](img/guide_connect_cluster.png)
## Copy credentials to GitLab.com project
-Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token.
+Once you have the Kubernetes Dashboard interface running, you should visit
+**Secrets** under the "Config" section. There, you should find the settings we
+need for GitLab integration: `ca.crt` and token.
![connect to cluster](img/guide_secret.png)
-You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you.
+You need to copy-paste the `ca.crt` and token into your project on GitLab.com in
+the Kubernetes integration page under project
+**Settings > Integrations > Project services > Kubernetes**. Don't actually copy
+the namespace though. Each project should have a unique namespace, and by leaving
+it blank, GitLab will create one for you.
![connect to cluster](img/guide_integration.png)
-For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform.
+For the API URL, you should use the "Endpoint" IP from your cluster page on
+Google Cloud Platform.
## Expose application to the world
-In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address.
+In order to be able to visit your application, you need to install an NGINX
+ingress controller and point your domain name to its external IP address. Let's
+see how that's done.
### Set up Ingress controller
@@ -68,28 +90,47 @@ helm init
helm install --name ruby-app stable/nginx-ingress
```
-This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that.
+This should create several services including `ruby-app-nginx-ingress-controller`.
+You can list your services by running `kubectl get svc` to confirm that.
### Point DNS at Cluster IP
-Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running:
+Find out the external IP address of the `ruby-app-nginx-ingress-controller` by
+running:
```sh
kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
-Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`.
+Use this IP address to configure your DNS. This part heavily depends on your
+preferences and domain provider. But in case you are not sure, just create an
+A record with a wildcard host like `*.<your-domain>`.
-Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP.
+Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is
+assigned to the cluster IP.
## Set up Auto DevOps
-In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save.
+In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
+section. Select "Enable Auto DevOps", add in your base domain, and save.
+
+Next, a pipeline needs to be triggered. Since the test project doesn't have a
+`.gitlab-ci.yml`, you need to either push a change to the repository or
+manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`,
+where `<username>` is your username.
-![auto devops settings](img/auto_devops_settings.png)
+This will create a new pipeline with several jobs: `build`, `test`, `codequality`,
+and `production`. The `build` job will create a Docker image with your new
+change and push it to the Container Registry. The `test` job will test your
+changes, whereas the `codequality` job will run static analysis on your changes.
+Finally, the `production` job will deploy your changes to a production application.
-Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab.
+Once the deploy job succeeds you should be able to see your application by
+visiting the Kubernetes dashboard. Select the namespace of your project, which
+will look like `minimal-ruby-app-23`, but with a unique ID for your project,
+and your app will be listed as "production" under the Deployment tab.
-Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!â€
+Once its ready, just visit `http://minimal-ruby-app.example.com` to see the
+famous "Hello, world!"!
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index a7db1f2e069..a7db1f2e069 100755..100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
index 03179ff5a77..03179ff5a77 100755..100644
--- a/doc/university/training/index.md
+++ b/doc/university/training/index.md
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 3ed601625cf..3ed601625cf 100755..100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index e6e4fea9b51..e6e4fea9b51 100755..100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
index a60c4365e0c..a60c4365e0c 100755..100644
--- a/doc/university/training/topics/bisect.md
+++ b/doc/university/training/topics/bisect.md
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
index af7a70a2818..af7a70a2818 100755..100644
--- a/doc/university/training/topics/cherry_picking.md
+++ b/doc/university/training/topics/cherry_picking.md
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
index 8149379b36f..8149379b36f 100755..100644
--- a/doc/university/training/topics/env_setup.md
+++ b/doc/university/training/topics/env_setup.md
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
index b65457728c0..b65457728c0 100755..100644
--- a/doc/university/training/topics/explore_gitlab.md
+++ b/doc/university/training/topics/explore_gitlab.md
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
index 4b34406ea75..4b34406ea75 100755..100644
--- a/doc/university/training/topics/feature_branching.md
+++ b/doc/university/training/topics/feature_branching.md
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
index ec7bb2631aa..ec7bb2631aa 100755..100644
--- a/doc/university/training/topics/getting_started.md
+++ b/doc/university/training/topics/getting_started.md
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
index 9ffb4b9c859..9ffb4b9c859 100755..100644
--- a/doc/university/training/topics/git_add.md
+++ b/doc/university/training/topics/git_add.md
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
index ca1ff29d93b..ca1ff29d93b 100755..100644
--- a/doc/university/training/topics/git_intro.md
+++ b/doc/university/training/topics/git_intro.md
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
index 32ebceff491..32ebceff491 100755..100644
--- a/doc/university/training/topics/git_log.md
+++ b/doc/university/training/topics/git_log.md
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index 8e5d3baf959..8e5d3baf959 100755..100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 77807b3e7ef..77807b3e7ef 100755..100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 5b446f02f63..5b446f02f63 100755..100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
index cf647284604..cf647284604 100755..100644
--- a/doc/university/training/topics/rollback_commits.md
+++ b/doc/university/training/topics/rollback_commits.md
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
index c1bdda32645..c1bdda32645 100755..100644
--- a/doc/university/training/topics/stash.md
+++ b/doc/university/training/topics/stash.md
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
index 5d869af64c1..5d869af64c1 100755..100644
--- a/doc/university/training/topics/subtree.md
+++ b/doc/university/training/topics/subtree.md
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index e9607b5a875..e9607b5a875 100755..100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index 17dbb64b9e6..17dbb64b9e6 100755..100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 9e38df26b6a..9e38df26b6a 100755..100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
new file mode 100644
index 00000000000..4a9384f3ad6
--- /dev/null
+++ b/doc/update/10.0-to-10.1.md
@@ -0,0 +1,356 @@
+# From 10.0 to 10.1
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-1-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-0-stable:config/gitlab.yml.example origin/10-1-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-0-stable:lib/support/nginx/gitlab-ssl origin/10-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-0-stable:lib/support/nginx/gitlab origin/10-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-0-stable:lib/support/init.d/gitlab.default.example origin/10-1-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (9.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 2abc57da1a0..baab217b6b7 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 3fd1d023d2a..6f1870a1366 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index 5f7a616cc7d..ce72b313031 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -194,7 +194,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 9d0b0da7edb..779ced0cf75 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -230,7 +230,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index 9ee01bc9c51..78d8a6c7de5 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -243,7 +243,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
index 1b5a15589af..a7255142ef5 100644
--- a/doc/update/9.4-to-9.5.md
+++ b/doc/update/9.4-to-9.5.md
@@ -252,7 +252,7 @@ ActionMailer::Base.delivery_method = :smtp
See [smtp_settings.rb.sample] as an example.
-[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md
new file mode 100644
index 00000000000..8581e6511f2
--- /dev/null
+++ b/doc/update/9.5-to-10.0.md
@@ -0,0 +1,356 @@
+# From 9.5 to 10.0
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-0-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-0-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.0 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-5-stable:config/gitlab.yml.example origin/10-0-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/9-5-stable:lib/support/nginx/gitlab-ssl origin/10-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/9-5-stable:lib/support/nginx/gitlab origin/10-0-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-5-stable:lib/support/init.d/gitlab.default.example origin/10-0-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (9.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index a7de5648c0e..fff47180099 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -1,78 +1,267 @@
-# Migrating GitLab from MySQL to Postgres
-*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.*
+---
+last_updated: 2017-10-05
+---
-If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this.
+# Migrating from MySQL to PostgreSQL
-## Export from MySQL and import into Postgres
+> **Note:** This guide assumes you have a working Omnibus GitLab instance with
+> MySQL and want to migrate to bundled PostgreSQL database.
-Use this if you are keeping GitLab on the same server.
+## Prerequisites
-```
-sudo service gitlab stop
+First, we'll need to enable the bundled PostgreSQL database with up-to-date
+schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data
+from the old MySQL database to the new PostgreSQL one.
-# Update /home/git/gitlab/config/database.yml
+Here's what you'll need to have installed:
-git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
-cd mysql-postgresql-converter
-mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
-python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql
-ed -s gitlabhq_production.psql < move_drop_indexes.ed
+- pgloader 3.4.1+
+- Omnibus GitLab
+- MySQL
-# Import the database dump as the application database user
-sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production
+## Enable bundled PostgreSQL database
-# Install gems for PostgreSQL (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+1. Stop GitLab:
-sudo service gitlab start
-```
+ ``` bash
+ sudo gitlab-ctl stop
+ ```
-## Converting a GitLab backup file from MySQL to Postgres
-**Note:** Please make sure to have Python 2.7.x (or higher) installed.
+1. Edit `/etc/gitlab/gitlab.rb` to enable bundled PostgreSQL:
-GitLab backup files (`<timestamp>_gitlab_backup.tar`) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server.
+ ```
+ postgresql['enable'] = true
+ ```
-```
-# Stop GitLab
-sudo service gitlab stop
+1. Edit `/etc/gitlab/gitlab.rb` to use the bundled PostgreSQL. Please check
+ all the settings beginning with `db_`, such as `gitlab_rails['db_adapter']`
+ and alike. You could just comment all of them out so that we'll just use
+ the defaults.
+
+1. [Reconfigure GitLab] for the changes to take effect:
+
+ ``` bash
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Start Unicorn and PostgreSQL so that we can prepare the schema:
+
+ ``` bash
+ sudo gitlab-ctl start unicorn
+ sudo gitlab-ctl start postgresql
+ ```
+
+1. Run the following commands to prepare the schema:
+
+ ``` bash
+ sudo gitlab-rake db:create db:migrate
+ ```
+
+1. Stop Unicorn to prevent other database access from interfering with the loading of data:
+
+ ``` bash
+ sudo gitlab-ctl stop unicorn
+ ```
+
+After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
+
+## Migrate data from MySQL to PostgreSQL
+
+Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
-# Create the backup
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+1. Save the following snippet in a `commands.load` file, and edit with your
+ database `username`, `password` and `host`:
-# Note the filename of the backup that was created. We will call it
-# TIMESTAMP_gitlab_backup.tar below.
+ ```
+ LOAD DATABASE
+ FROM mysql://username:password@host/gitlabhq_production
+ INTO postgresql://gitlab-psql@unix://var/opt/gitlab/postgresql:/gitlabhq_production
-# Move the backup file we will convert to its own directory
-sudo -u git -H mkdir -p tmp/backups/postgresql
-sudo -u git -H mv tmp/backups/TIMESTAMP_gitlab_backup.tar tmp/backups/postgresql/
+ WITH include no drop, truncate, disable triggers, create no tables,
+ create no indexes, preserve index names, no foreign keys,
+ data only
-# Create a separate database dump with PostgreSQL compatibility
-cd tmp/backups/postgresql
-sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
+ ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
-# Clone the database converter
-sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
+ ;
+ ```
-# Convert gitlabhq_production.mysql
-sudo -u git -H mkdir db
-sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql
-sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed
+1. Start the migration:
-# Compress database backup
-# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with:
-# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql
-# The compressed databasedump is not supported at 7.12.0 and older.
-sudo -u git -H gzip db/database.sql
+ ``` bash
+ sudo -u gitlab-psql pgloader commands.load
+ ```
-# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar.
+1. Once the migration finishes, you should see a summary table that looks like
+the following:
-# Warning: if you forget to replace TIMESTAMP below, tar will create a new file
-# 'TIMESTAMP_gitlab_backup.tar' without giving an error.
-sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz
+ ```
+ table name read imported errors total time
+ ----------------------------------------------- --------- --------- --------- --------------
+ fetch meta data 119 119 0 0.388s
+ Truncate 119 119 0 1.134s
+ ----------------------------------------------- --------- --------- --------- --------------
+ public.abuse_reports 0 0 0 0.490s
+ public.appearances 0 0 0 0.488s
+ public.approvals 0 0 0 0.273s
+ public.application_settings 1 1 0 0.266s
+ public.approvers 0 0 0 0.339s
+ public.approver_groups 0 0 0 0.357s
+ public.audit_events 1 1 0 0.410s
+ public.award_emoji 0 0 0 0.441s
+ public.boards 0 0 0 0.505s
+ public.broadcast_messages 0 0 0 0.498s
+ public.chat_names 0 0 0 0.576s
+ public.chat_teams 0 0 0 0.617s
+ public.ci_builds 0 0 0 0.611s
+ public.ci_group_variables 0 0 0 0.620s
+ public.ci_pipelines 0 0 0 0.599s
+ public.ci_pipeline_schedules 0 0 0 0.622s
+ public.ci_pipeline_schedule_variables 0 0 0 0.573s
+ public.ci_pipeline_variables 0 0 0 0.594s
+ public.ci_runners 0 0 0 0.533s
+ public.ci_runner_projects 0 0 0 0.584s
+ public.ci_sources_pipelines 0 0 0 0.564s
+ public.ci_stages 0 0 0 0.595s
+ public.ci_triggers 0 0 0 0.569s
+ public.ci_trigger_requests 0 0 0 0.596s
+ public.ci_variables 0 0 0 0.565s
+ public.container_repositories 0 0 0 0.605s
+ public.conversational_development_index_metrics 0 0 0 0.571s
+ public.deployments 0 0 0 0.607s
+ public.emails 0 0 0 0.602s
+ public.deploy_keys_projects 0 0 0 0.557s
+ public.events 160 160 0 0.677s
+ public.environments 0 0 0 0.567s
+ public.features 0 0 0 0.639s
+ public.events_for_migration 160 160 0 0.582s
+ public.feature_gates 0 0 0 0.579s
+ public.forked_project_links 0 0 0 0.660s
+ public.geo_nodes 0 0 0 0.686s
+ public.geo_event_log 0 0 0 0.626s
+ public.geo_repositories_changed_events 0 0 0 0.677s
+ public.geo_node_namespace_links 0 0 0 0.618s
+ public.geo_repository_renamed_events 0 0 0 0.696s
+ public.gpg_keys 0 0 0 0.704s
+ public.geo_repository_deleted_events 0 0 0 0.638s
+ public.historical_data 0 0 0 0.729s
+ public.geo_repository_updated_events 0 0 0 0.634s
+ public.index_statuses 0 0 0 0.746s
+ public.gpg_signatures 0 0 0 0.667s
+ public.issue_assignees 80 80 0 0.769s
+ public.identities 0 0 0 0.655s
+ public.issue_metrics 80 80 0 0.781s
+ public.issues 80 80 0 0.720s
+ public.labels 0 0 0 0.795s
+ public.issue_links 0 0 0 0.707s
+ public.label_priorities 0 0 0 0.793s
+ public.keys 0 0 0 0.734s
+ public.lfs_objects 0 0 0 0.812s
+ public.label_links 0 0 0 0.725s
+ public.licenses 0 0 0 0.813s
+ public.ldap_group_links 0 0 0 0.751s
+ public.members 52 52 0 0.830s
+ public.lfs_objects_projects 0 0 0 0.738s
+ public.merge_requests_closing_issues 0 0 0 0.825s
+ public.lists 0 0 0 0.769s
+ public.merge_request_diff_commits 0 0 0 0.840s
+ public.merge_request_metrics 0 0 0 0.837s
+ public.merge_requests 0 0 0 0.753s
+ public.merge_request_diffs 0 0 0 0.771s
+ public.namespaces 30 30 0 0.874s
+ public.merge_request_diff_files 0 0 0 0.775s
+ public.notes 0 0 0 0.849s
+ public.milestones 40 40 0 0.799s
+ public.oauth_access_grants 0 0 0 0.979s
+ public.namespace_statistics 0 0 0 0.797s
+ public.oauth_applications 0 0 0 0.899s
+ public.notification_settings 72 72 0 0.818s
+ public.oauth_access_tokens 0 0 0 0.807s
+ public.pages_domains 0 0 0 0.958s
+ public.oauth_openid_requests 0 0 0 0.832s
+ public.personal_access_tokens 0 0 0 0.965s
+ public.projects 8 8 0 0.987s
+ public.path_locks 0 0 0 0.925s
+ public.plans 0 0 0 0.923s
+ public.project_features 8 8 0 0.985s
+ public.project_authorizations 66 66 0 0.969s
+ public.project_import_data 8 8 0 1.002s
+ public.project_statistics 8 8 0 1.001s
+ public.project_group_links 0 0 0 0.949s
+ public.project_mirror_data 0 0 0 0.972s
+ public.protected_branch_merge_access_levels 0 0 0 1.017s
+ public.protected_branches 0 0 0 0.969s
+ public.protected_branch_push_access_levels 0 0 0 0.991s
+ public.protected_tags 0 0 0 1.009s
+ public.protected_tag_create_access_levels 0 0 0 0.985s
+ public.push_event_payloads 0 0 0 1.041s
+ public.push_rules 0 0 0 0.999s
+ public.redirect_routes 0 0 0 1.020s
+ public.remote_mirrors 0 0 0 1.034s
+ public.releases 0 0 0 0.993s
+ public.schema_migrations 896 896 0 1.057s
+ public.routes 38 38 0 1.021s
+ public.services 0 0 0 1.055s
+ public.sent_notifications 0 0 0 1.003s
+ public.slack_integrations 0 0 0 1.022s
+ public.spam_logs 0 0 0 1.024s
+ public.snippets 0 0 0 1.058s
+ public.subscriptions 0 0 0 1.069s
+ public.taggings 0 0 0 1.099s
+ public.timelogs 0 0 0 1.104s
+ public.system_note_metadata 0 0 0 1.038s
+ public.tags 0 0 0 1.034s
+ public.trending_projects 0 0 0 1.140s
+ public.uploads 0 0 0 1.129s
+ public.todos 80 80 0 1.085s
+ public.users_star_projects 0 0 0 1.153s
+ public.u2f_registrations 0 0 0 1.061s
+ public.web_hooks 0 0 0 1.179s
+ public.users 26 26 0 1.163s
+ public.user_agent_details 0 0 0 1.068s
+ public.web_hook_logs 0 0 0 1.080s
+ ----------------------------------------------- --------- --------- --------- --------------
+ COPY Threads Completion 4 4 0 2.008s
+ Reset Sequences 113 113 0 0.304s
+ Install Comments 0 0 0 0.000s
+ ----------------------------------------------- --------- --------- --------- --------------
+ Total import time 1894 1894 0 12.497s
+ ```
+
+ If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See
+ the [troubleshooting guide](#Troubleshooting) for more details.
+
+1. Start GitLab:
+
+ ``` bash
+ sudo gitlab-ctl start
+ ```
+
+Now, you can verify that everything worked by visiting GitLab.
+
+## Troubleshooting
+
+### Permissions
+
+Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see
+a similar message to the following:
-# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab
-# installation.
-# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups.
```
+debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
+ #<THREAD "lparallel" RUNNING {10078A3513}>:
+ Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger
+ QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL;
+ 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger
+ QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL;
+```
+
+### Experiencing 500 errors after the migration
+
+If you experience 500 errors after the migration, try to clear the cache:
+
+``` bash
+sudo gitlab-rake cache:clear
+```
+
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 30107360446..b2679d1ff22 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -74,7 +74,15 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
-### 5. Update gitlab-shell to the corresponding version
+### 5. Update gitaly to the corresponding version
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production
+```
+
+### 6. Update gitlab-shell to the corresponding version
```bash
cd /home/git/gitlab-shell
@@ -84,14 +92,14 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca
sudo -u git -H sh -c 'if [ -x bin/compile ]; then bin/compile; fi'
```
-### 6. Start application
+### 7. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
-### 7. Check application status
+### 8. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md
index 3d93c7557a4..a98602c4d70 100644
--- a/doc/user/admin_area/monitoring/convdev.md
+++ b/doc/user/admin_area/monitoring/convdev.md
@@ -23,7 +23,7 @@ If you have just started using GitLab, it may take a few weeks for data to be
collected before this feature is available.
This feature is accessible only to a system admin, at
-**Admin area > Monitoring > ConvDev Index**.
+**Admin area > Overview > ConvDev Index**.
[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469
[ping]: ../settings/usage_statistics.md#usage-ping
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
index 70934f9960a..843fb4ce26b 100644
--- a/doc/user/admin_area/monitoring/health_check.md
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -18,7 +18,7 @@ traffic until the system is ready or restart the container as needed.
To access monitoring resources, the client IP needs to be included in a whitelist.
-[Read how to add IPs to a whitelist for the monitoring endpoints.][admin].
+[Read how to add IPs to a whitelist for the monitoring endpoints][admin].
## Using the endpoint
diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png
index 4e47ff2228d..ffe18d76c96 100644
--- a/doc/user/admin_area/monitoring/img/convdev_index.png
+++ b/doc/user/admin_area/monitoring/img/convdev_index.png
Binary files differ
diff --git a/doc/user/discussions/img/discussion_lock_system_notes.png b/doc/user/discussions/img/discussion_lock_system_notes.png
new file mode 100644
index 00000000000..8e8e8e0bc3d
--- /dev/null
+++ b/doc/user/discussions/img/discussion_lock_system_notes.png
Binary files differ
diff --git a/doc/user/discussions/img/lock_form_member.png b/doc/user/discussions/img/lock_form_member.png
new file mode 100644
index 00000000000..01c6308d24c
--- /dev/null
+++ b/doc/user/discussions/img/lock_form_member.png
Binary files differ
diff --git a/doc/user/discussions/img/lock_form_non_member.png b/doc/user/discussions/img/lock_form_non_member.png
new file mode 100644
index 00000000000..3bb70b69580
--- /dev/null
+++ b/doc/user/discussions/img/lock_form_non_member.png
Binary files differ
diff --git a/doc/user/discussions/img/turn_off_lock.png b/doc/user/discussions/img/turn_off_lock.png
new file mode 100644
index 00000000000..dd05b398a8b
--- /dev/null
+++ b/doc/user/discussions/img/turn_off_lock.png
Binary files differ
diff --git a/doc/user/discussions/img/turn_on_lock.png b/doc/user/discussions/img/turn_on_lock.png
new file mode 100644
index 00000000000..9597da4e14d
--- /dev/null
+++ b/doc/user/discussions/img/turn_on_lock.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index efea99eb120..ab46befb91c 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -153,12 +153,52 @@ comments in greater detail.
![Discussion comment](img/discussion_comment.png)
+## Locking discussions
+
+> [Introduced][ce-14531] in GitLab 10.1.
+
+There might be some cases where a discussion is better off if it's locked down.
+For example:
+
+- Discussions that are several years old and the issue/merge request is closed,
+ but people continue to try to resurrect the discussion.
+- Discussions where someone or a group of people are trolling, are abusive, or
+ in-general are causing the discussion to be unproductive.
+
+In locked discussions, only team members can write new comments and edit the old
+ones.
+
+To lock or unlock a discussion, you need to have at least Master [permissions]:
+
+1. Find the "Lock" section in the sidebar and click **Edit**
+1. In the dialog that will appear, you can choose to turn on or turn off the
+ discussion lock
+1. Optionally, leave a comment to explain your reasoning behind that action
+
+| Turn off discussion lock | Turn on discussion lock |
+| :-----------: | :----------: |
+| ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) |
+
+Every change is indicated by a system note in the issue's or merge request's
+comments.
+
+![Discussion lock system notes](img/discussion_lock_system_notes.png)
+
+Once an issue or merge request is locked, project members can see the indicator
+in the comment area, whereas non project members can only see the information
+that the discussion is locked.
+
+| Team member | Not a member |
+| :-----------: | :----------: |
+| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
+[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index db0242f1324..a1671f9dd91 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -188,6 +188,27 @@ Besides giving you the option to edit any settings you've previously
set when [creating the group](#create-a-new-group), you can also
access further configurations for your group.
+#### Changing a group's path
+
+> **Note:** If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing a group's path or renaming a
+username, you can create a new group and transfer projects to it.
+
+Changing a group's path can have unintended side effects.
+
+* Existing web URLs for the group and anything under it (i.e. projects) will
+redirect to the new URLs
+* Existing Git remote URLs for projects under the group will no longer work, but
+Git responses will show an error with the new remote URL
+* The original namespace can be claimed again by any group or user, which will
+destroy web redirects and Git remote warnings
+* If you are vacating the path so it can be claimed by another group or user,
+you may need to rename the group name as well since both names and paths must be
+unique
+
+> It is currently not possible to rename a namespace if it contains a
+project with container registry tags, because the project cannot be moved.
+
#### Enforce 2FA to group members
Add a security layer to your group by
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index d2478aea4bd..161a3af9903 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -84,10 +84,13 @@ structure.
a subgroup. For more information check the [permissions table][permissions].
- For a list of words that are not allowed to be used as group names see the
[reserved names][reserved].
+- Users can always create subgroups if they are explicitly added as an Owner to
+ a parent group even if group creation is disabled by an administrator in their
+ settings.
To create a subgroup:
-1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**.
+1. In the group's dashboard go to the **Subgroups** page and click **New subgroup**.
![Subgroups page](img/create_subgroup_button.png)
@@ -100,9 +103,7 @@ To create a subgroup:
1. Click the **Create group** button and you will be taken to the new group's
dashboard page.
----
-
-You can follow the same process to create any subsequent groups.
+Follow the same process to create any subsequent groups.
## Membership
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index b42b8f0a525..454988b9b80 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -596,6 +596,30 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
+#### Details and Summary
+
+Content can be collapsed using HTML's [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) and [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary) tags. This is especially useful for collapsing long logs so they take up less screen space.
+
+<p>
+<details>
+<summary>Click me to collapse/fold.</summary>
+These details will remain hidden until expanded.
+
+<pre><code>PASTE LOGS HERE</code></pre>
+</details>
+</p>
+
+**Note:** Unfortunately Markdown is not supported inside these tags, as described by the [markdown specification](https://daringfireball.net/projects/markdown/syntax#html). You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting).
+
+```html
+<details>
+<summary>Click me to collapse/fold.</summary>
+These details will remain hidden until expanded.
+
+<pre><code>PASTE LOGS HERE</code></pre>
+</details>
+```
+
### Horizontal Rule
```
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0c17905aa8c..be72d42625a 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -25,6 +25,7 @@ The following table depicts the various user permission levels in a project.
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Lock comments | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
@@ -71,6 +72,7 @@ The following table depicts the various user permission levels in a project.
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
+| Delete issues | | | | | ✓ |
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ |
@@ -253,7 +255,7 @@ only.
[^1]: On public and internal projects, all users are able to perform this action.
[^2]: Guest users can only view the confidential issues they created themselves
-[^3]: If **Public pipelines** is enabled in **Project Settings > Pipelines**
+[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 7d25970fcb1..5ebb88bf324 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -8,10 +8,27 @@ experience according to the best approach to their cases.
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
related to your user ID.
+### Changing your username
+
You can change your `username` from your
-[profile settings](#profile-settings). To avoid breaking
-paths when you change your `username`, we suggest you follow
-[this procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
+[profile settings](#profile-settings).
+
+> **Note:** If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing your username, you can
+create a new group and transfer projects to it.
+Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
+
+Changing your username can have unintended side effects.
+
+* Existing web URLs for the user and anything under it (i.e. projects) will
+redirect to the new URLs
+* Existing Git remote URLs for projects under the user will no longer work, but
+Git responses will show an error with the new remote URL
+* The original namespace can be claimed again by any group or user, which will
+destroy any web redirects and Git remote warnings
+
+> It is currently not possible to rename a namespace if it contains a
+project with container registry tags, because the project cannot be moved.
## User profile
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 5c615daf464..2c4dfcff4a6 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -17,25 +17,25 @@ have its own space to store its Docker images.
You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
----
-
## Enable the Container Registry for your project
+NOTE: **Note:**
+If you cannot find the Container Registry entry under your project's settings,
+that means that it is not enabled in your GitLab instance. Ask your administrator
+to enable it.
+
1. First, ask your system administrator to enable GitLab Container Registry
following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using
the Registry immediately.
-
-1. Go to your project's settings and enable the **Container Registry** feature
- on your project. For new projects this might be enabled by default. For
- existing projects (prior GitLab 8.8), you will have to explicitly enable it.
-
- ![Enable Container Registry](img/container_registry_enable.png)
-
+1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
+ and enable the **Container Registry** feature on your project. For new
+ projects this might be enabled by default. For existing projects
+ (prior GitLab 8.8), you will have to explicitly enable it.
1. Hit **Save changes** for the changes to take effect. You should now be able
- to see the **Registry** link in the project menu.
+ to see the **Registry** link in the sidebar.
- ![Container Registry tab](img/container_registry_tab.png)
+![Container Registry](img/container_registry.png)
## Build and push images
diff --git a/doc/user/project/img/container_registry.png b/doc/user/project/img/container_registry.png
new file mode 100644
index 00000000000..abbaf838538
--- /dev/null
+++ b/doc/user/project/img/container_registry.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
deleted file mode 100644
index d067a8be1ca..00000000000
--- a/doc/user/project/img/container_registry_enable.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
deleted file mode 100644
index a85237271d9..00000000000
--- a/doc/user/project/img/container_registry_tab.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index cf7f519f783..5f6dc9e4e8b 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png
index c6b17ada40e..3666dbb87ab 100644
--- a/doc/user/project/img/issue_board_move_issue_card_list.png
+++ b/doc/user/project/img/issue_board_move_issue_card_list.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png
deleted file mode 100644
index badfbed0bbe..00000000000
--- a/doc/user/project/img/labels_assign_label_in_new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png
index 474953d565b..7934e3bfb5e 100644
--- a/doc/user/project/img/labels_default.png
+++ b/doc/user/project/img/labels_default.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png
index 3aca77f0070..6a1ebfc2ecb 100644
--- a/doc/user/project/img/labels_filter.png
+++ b/doc/user/project/img/labels_filter.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
index 5609a1f6d7f..419e555e709 100644
--- a/doc/user/project/img/labels_filter_by_priority.png
+++ b/doc/user/project/img/labels_filter_by_priority.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png
index b44b4bd296d..e26425d0188 100644
--- a/doc/user/project/img/labels_new_label.png
+++ b/doc/user/project/img/labels_new_label.png
Binary files differ
diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png
index 3e888f36364..d602a3c90ec 100644
--- a/doc/user/project/img/labels_prioritize.png
+++ b/doc/user/project/img/labels_prioritize.png
Binary files differ
diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png
index 1aa7efc36f1..aa4d4452c87 100644
--- a/doc/user/project/img/project_repository_settings.png
+++ b/doc/user/project/img/project_repository_settings.png
Binary files differ
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 016f98966e3..6423beefc77 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -42,6 +42,11 @@ The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's
namespace that started the import process.
+The importer will also import branches on forks of projects related to open pull
+requests. These branches will be imported with a naming scheume similar to
+GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
+in branches compared to the GitHub Repository.
+
## Importing your GitHub repositories
The importer page is visible when you create a new project.
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index d6b3d59d407..03bbc46bd8c 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -20,6 +20,8 @@ When you create a project in GitLab, you'll have access to a large number of
- [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) (**EES/EEP**): Allow your teams to create their own workflows (Issue Boards) for the same project
- [Repositories](repository/index.md): Host your code in a fully
integrated platform
+ - [Branches](repository/branches/index.md): use Git branching strategies to
+ collaborate on code
- [Protected branches](protected_branches.md): Prevent collaborators
from messing with history or pushing code without review
- [Protected tags](protected_tags.md): Control over who has
@@ -87,6 +89,10 @@ You can [fork a project](../../gitlab-basics/fork-project.md) in order to:
from your fork to the upstream project
- Fork a sample project to work on the top of that
+## Project settings
+
+Read through the documentation on [project settings](settings/index.md).
+
## Import or export a project
- [Import a project](import/index.md) from:
diff --git a/doc/user/project/integrations/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
index 349a2dc8456..e535e2b8d46 100644
--- a/doc/user/project/integrations/img/kubernetes_configuration.png
+++ b/doc/user/project/integrations/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png
index 917068d9398..917068d9398 100755..100644
--- a/doc/user/project/integrations/img/webhook_logs.png
+++ b/doc/user/project/integrations/img/webhook_logs.png
Binary files differ
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index f4000523938..e9738b683f9 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -1,3 +1,7 @@
+---
+last_updated: 2017-09-25
+---
+
# GitLab Kubernetes / OpenShift integration
GitLab can be configured to interact with Kubernetes, or other systems using the
@@ -6,62 +10,114 @@ Kubernetes API (such as OpenShift).
Each project can be configured to connect to a different Kubernetes cluster, see
the [configuration](#configuration) section.
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see the [Services Templates](services_templates.md) document.
-
## Configuration
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-of your project and select the **Kubernetes** service to configure it.
+of your project and select the **Kubernetes** service to configure it. Fill in
+all the needed parameters, check the "Active" checkbox and hit **Save changes**
+for the changes to take effect.
![Kubernetes configuration settings](img/kubernetes_configuration.png)
-The Kubernetes service takes the following arguments:
-
-1. API URL
-1. Custom CA bundle
-1. Kubernetes namespace
-1. Service token
-
-The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
-exposes several APIs - we want the "base" URL that is common to all of them,
-e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-
-GitLab authenticates against Kubernetes using service tokens, which are
-scoped to a particular `namespace`. If you don't have a service token yet,
-you can follow the
-[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
-to create one. You can also view or create service tokens in the
-[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
-`Config -> Secrets`.
-
-Fill in the service token and namespace according to the values you just got.
-If the API is using a self-signed TLS certificate, you'll also need to include
-the `ca.crt` contents as the `Custom CA bundle`.
+The Kubernetes service takes the following parameters:
+
+- **API URL** -
+ It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
+ exposes several APIs, we want the "base" URL that is common to all of them,
+ e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+- **CA certificate** (optional) -
+ If the API is using a self-signed TLS certificate, you'll also need to include
+ the `ca.crt` contents here.
+- **Project namespace** (optional) - The following apply:
+ - By default you don't have to fill it in; by leaving it blank, GitLab will
+ create one for you.
+ - Each project should have a unique namespace.
+ - The project namespace is not necessarily the namespace of the secret, if
+ you're using a secret with broader permissions, like the secret from `default`.
+ - You should **not** use `default` as the project namespace.
+ - If you or someone created a secret specifically for the project, usually
+ with limited permissions, the secret's namespace and project namespace may
+ be the same.
+- **Token** -
+ GitLab authenticates against Kubernetes using service tokens, which are
+ scoped to a particular `namespace`. If you don't have a service token yet,
+ you can follow the
+ [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
+ to create one. You can also view or create service tokens in the
+ [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
+ (under **Config > Secrets**).
+
+TIP: **Tip:**
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see [Services Templates](services_templates.md).
## Deployment variables
-The Kubernetes service exposes following
+The Kubernetes service exposes the following
[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
-GitLab CI build environment:
+GitLab CI/CD build environment:
-- `KUBE_URL` - equal to the API URL
-- `KUBE_TOKEN`
+- `KUBE_URL` - Equal to the API URL.
+- `KUBE_TOKEN` - The Kubernetes token.
- `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
+- `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.
-- `KUBECONFIG` - Path to a file containing kubeconfig for this deployment. CA bundle would be embedded if specified.
+- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
+- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
+ CA bundle would be embedded if specified.
+
+## What you can get with the Kubernetes integration
+
+Here's what you can do with GitLab if you enable the Kubernetes integration.
+
+### Deploy Boards (EEP)
+
+> Available in [GitLab Enterprise Edition Premium][ee].
-## Web terminals
+GitLab's Deploy Boards offer a consolidated view of the current health and
+status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
+displaying the status of the pods in the deployment. Developers and other
+teammates can view the progress and status of a rollout, pod by pod, in the
+workflow they already use without any need to access Kubernetes.
->**NOTE:**
-Added in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is currently limited to the first container in the
+[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
+
+### Canary Deployments (EEP)
+
+> Available in [GitLab Enterprise Edition Premium][ee].
+
+Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
+and visualize your canary deployments right inside the Deploy Board, without
+the need to leave GitLab.
+
+[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
+
+### Kubernetes monitoring
+
+Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
+[NGINX ingress](./prometheus_library/nginx.md) is also supported.
+
+[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md)
+
+### Auto DevOps
+
+Auto DevOps automatically detects, builds, tests, deploys, and monitors your
+applications.
+
+To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
+you will need the Kubernetes project integration enabled.
+
+[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
+
+### Web terminals
+
+NOTE: **Note:**
+Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is limited to the first container in the
first pod of your environment.
When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
@@ -70,3 +126,5 @@ Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+
+[ee]: https://about.gitlab.com/gitlab-ee/
diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md
index cc5cee36d28..34a0b97a171 100644
--- a/doc/user/project/integrations/prometheus_library/cloudwatch.md
+++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md
@@ -1,8 +1,13 @@
# Monitoring AWS Resources
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4
GitLab has support for automatically detecting and monitoring AWS resources, starting with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). This is provided by leveraging the official [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter), which translates [Cloudwatch metrics](https://aws.amazon.com/cloudwatch/) into a Prometheus readable form.
+## Requirements
+
+The [Prometheus service](../prometheus/index.md) must be enabled.
+
## Metrics supported
| Name | Query |
diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md
index f2939f047a3..518018e5839 100644
--- a/doc/user/project/integrations/prometheus_library/haproxy.md
+++ b/doc/user/project/integrations/prometheus_library/haproxy.md
@@ -3,11 +3,15 @@
GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form.
+## Requirements
+
+The [Prometheus service](../prometheus/index.md) must be enabled.
+
## Metrics supported
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) |
+| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code) |
| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for HAProxy metrics
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 9f0308d8111..518683965e8 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -1,14 +1,20 @@
# Monitoring Kubernetes
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0
-GitLab has support for automatically detecting and monitoring Kubernetes metrics. Kubernetes exposes Node level metrics out of the box via the built-in [Prometheus metrics support in cAdvisor](https://github.com/google/cadvisor). No additional services or exporters are needed.
+GitLab has support for automatically detecting and monitoring Kubernetes metrics.
+
+## Requirements
+
+The [Prometheus](../prometheus.md) and [Kubernetes](../kubernetes.md)
+integration services must be enabled.
## Metrics supported
| Name | Query |
| ---- | ----- |
| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 |
-| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}) * 100 |
+| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics
@@ -23,4 +29,4 @@ Prometheus server up and running. You have two options here:
In order to isolate and only display relevant metrics for a given environment
however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments).
-If you are using [GitLab Auto-Deploy][../../../ci/autodeploy/index.md] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added.
+If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added.
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index 12e3321f5f3..7fb8369d3c1 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -1,13 +1,18 @@
# Monitoring NGINX
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4
GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form.
+## Requirements
+
+The [Prometheus service](../prometheus/index.md) must be enabled.
+
## Metrics supported
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) |
+| Throughput (req/sec) | sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 84ee8bc45e5..e6f13d0630b 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -1,25 +1,44 @@
# Monitoring NGINX Ingress Controller
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress.
+## Requirements
+
+The [Prometheus service](../prometheus/index.md) must be enabled.
+
## Metrics supported
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
+| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
-## Configuring Prometheus to monitor for NGINX ingress metrics
+## Configuring NGINX ingress monitoring
+
+If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, and your application is running in the same cluster, no further action is required. The ingress metrics will be automatically enabled and annotated for Prometheus monitoring. Simply ensure Prometheus monitoring is [enabled for your project](../prometheus.md), which is on by default.
+
+For other deployments, there is some configuration required depending on your installation:
+* NGINX Ingress should be version 0.9.0 or above
+* NGINX Ingress should be annotated for Prometheus monitoring
+* Prometheus should be configured to monitor annotated pods
+
+### Setting up NGINX Ingress for Prometheus monitoring
+
+Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254.
+
+With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. Since NGINX ingress metrics are exposed per pod, a scrape job for Kubernetes pods is required. A sample pod scraping configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring.
-The easiest way to get started is to use at least version 0.9.0 of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). If you are using NGINX as your Kubernetes ingress, there is [direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release.
+Depending on how NGINX ingress was deployed, typically a DaemonSet or Deployment, edit the corresponding YML spec. Two new annotations need to be added:
+* `prometheus.io/scrape: "true"`
+* `prometheus.io/port: "10254"`
-If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, these metrics will be automatically enabled and annotated for Prometheus monitoring.
+Prometheus should now be collecting NGINX ingress metrics. To validate view the Prometheus Targets, available under `Status > Targets` on the Prometheus dashboard. New entries for NGINX should be listed in the kubernetes pod monitoring job, `kubernetes-pods`.
## Specifying the Environment label
-In order to isolate and only display relevant metrics for a given environment
-however, GitLab needs a method to detect which labels are associated. To do this, GitLab will search metrics with appropriate labels. In this case, the `upstream` label must be of the form `<Kubernetes Namespace>-<CI_ENVIRONMENT_SLUG>-*`.
+In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`.
If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index e2cc67726e0..96a5a23ee13 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -12,6 +12,8 @@ Other interesting links:
- [GitLab Issue Board landing page on about.gitlab.com][landing]
- [YouTube video introduction to Issue Boards][youtube]
+![GitLab Issue Board](img/issue_board.png)
+
## Overview
The Issue Board builds on GitLab's existing
@@ -89,10 +91,6 @@ two defaults:
- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left.
- **Closed** (default): shows all closed issues. Always appears on the very right.
-![GitLab Issue Board](img/issue_board.png)
-
----
-
In short, here's a list of actions you can take in an Issue Board:
- [Create a new list](#creating-a-new-list).
diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md
new file mode 100644
index 00000000000..d7442104c53
--- /dev/null
+++ b/doc/user/project/issues/deleting_issues.md
@@ -0,0 +1,11 @@
+# Deleting Issues
+
+> [Introduced][ce-2982] in GitLab 8.6
+
+Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
+
+You can delete an issue by editing it and clicking on the delete button.
+
+![delete issue - button](img/delete_issue.png)
+
+>**Note:** Only [project owners](../../permissions.md) can delete issues. \ No newline at end of file
diff --git a/doc/user/project/issues/img/button_close_issue.png b/doc/user/project/issues/img/button_close_issue.png
index 8fb2e23f58a..05d257ce9bf 100755..100644
--- a/doc/user/project/issues/img/button_close_issue.png
+++ b/doc/user/project/issues/img/button_close_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/closing_and_related_issues.png b/doc/user/project/issues/img/closing_and_related_issues.png
index c6543e85fdb..c6543e85fdb 100755..100644
--- a/doc/user/project/issues/img/closing_and_related_issues.png
+++ b/doc/user/project/issues/img/closing_and_related_issues.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_create.png b/doc/user/project/issues/img/confidential_issues_create.png
index 0a141eb39f8..0a141eb39f8 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_create.png
+++ b/doc/user/project/issues/img/confidential_issues_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_guest.png b/doc/user/project/issues/img/confidential_issues_search_guest.png
index dc1b4ba8ad7..dc1b4ba8ad7 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_search_guest.png
+++ b/doc/user/project/issues/img/confidential_issues_search_guest.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_master.png b/doc/user/project/issues/img/confidential_issues_search_master.png
index fc01f4da9db..fc01f4da9db 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_search_master.png
+++ b/doc/user/project/issues/img/confidential_issues_search_master.png
Binary files differ
diff --git a/doc/user/project/issues/img/delete_issue.png b/doc/user/project/issues/img/delete_issue.png
new file mode 100644
index 00000000000..a356f52044e
--- /dev/null
+++ b/doc/user/project/issues/img/delete_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_create.png b/doc/user/project/issues/img/due_dates_create.png
index ece35d44213..ece35d44213 100755..100644
--- a/doc/user/project/issues/img/due_dates_create.png
+++ b/doc/user/project/issues/img/due_dates_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_edit_sidebar.png b/doc/user/project/issues/img/due_dates_edit_sidebar.png
index d1c7d1eb7e9..d1c7d1eb7e9 100755..100644
--- a/doc/user/project/issues/img/due_dates_edit_sidebar.png
+++ b/doc/user/project/issues/img/due_dates_edit_sidebar.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_issues_index_page.png b/doc/user/project/issues/img/due_dates_issues_index_page.png
index 94679436b32..94679436b32 100755..100644
--- a/doc/user/project/issues/img/due_dates_issues_index_page.png
+++ b/doc/user/project/issues/img/due_dates_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_todos.png b/doc/user/project/issues/img/due_dates_todos.png
index 4c124c97f67..4c124c97f67 100755..100644
--- a/doc/user/project/issues/img/due_dates_todos.png
+++ b/doc/user/project/issues/img/due_dates_todos.png
Binary files differ
diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png
index 5d20e8cbc89..bba964076d0 100644
--- a/doc/user/project/issues/img/group_issues_list_view.png
+++ b/doc/user/project/issues/img/group_issues_list_view.png
Binary files differ
diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png
index 1759b28a9ef..87b1016cc76 100755..100644
--- a/doc/user/project/issues/img/issue_board.png
+++ b/doc/user/project/issues/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png
index c63229a4af2..0e4c8df897b 100755..100644
--- a/doc/user/project/issues/img/issue_template.png
+++ b/doc/user/project/issues/img/issue_template.png
Binary files differ
diff --git a/doc/user/project/issues/img/issues_main_view.png b/doc/user/project/issues/img/issues_main_view.png
index 4faa42e40ee..a929916c682 100644
--- a/doc/user/project/issues/img/issues_main_view.png
+++ b/doc/user/project/issues/img/issues_main_view.png
Binary files differ
diff --git a/doc/user/project/issues/img/issues_main_view_numbered.jpg b/doc/user/project/issues/img/issues_main_view_numbered.jpg
index 4b5d7fba459..b4b68476d24 100644
--- a/doc/user/project/issues/img/issues_main_view_numbered.jpg
+++ b/doc/user/project/issues/img/issues_main_view_numbered.jpg
Binary files differ
diff --git a/doc/user/project/issues/img/mention_in_issue.png b/doc/user/project/issues/img/mention_in_issue.png
index c762a812138..c762a812138 100755..100644
--- a/doc/user/project/issues/img/mention_in_issue.png
+++ b/doc/user/project/issues/img/mention_in_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/mention_in_merge_request.png b/doc/user/project/issues/img/mention_in_merge_request.png
index 681e086d6e0..681e086d6e0 100755..100644
--- a/doc/user/project/issues/img/mention_in_merge_request.png
+++ b/doc/user/project/issues/img/mention_in_merge_request.png
Binary files differ
diff --git a/doc/user/project/issues/img/merge_request_closes_issue.png b/doc/user/project/issues/img/merge_request_closes_issue.png
index 6fd27738843..6fd27738843 100755..100644
--- a/doc/user/project/issues/img/merge_request_closes_issue.png
+++ b/doc/user/project/issues/img/merge_request_closes_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/new_issue.png b/doc/user/project/issues/img/new_issue.png
index e72ac49d6b9..07d65a93070 100755..100644
--- a/doc/user/project/issues/img/new_issue.png
+++ b/doc/user/project/issues/img/new_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/new_issue_from_issue_board.png b/doc/user/project/issues/img/new_issue_from_issue_board.png
index 9c2b3ff50fa..da892eff0a6 100755..100644
--- a/doc/user/project/issues/img/new_issue_from_issue_board.png
+++ b/doc/user/project/issues/img/new_issue_from_issue_board.png
Binary files differ
diff --git a/doc/user/project/issues/img/new_issue_from_open_issue.png b/doc/user/project/issues/img/new_issue_from_open_issue.png
index 2aed5372830..c6f3f0617ab 100755..100644
--- a/doc/user/project/issues/img/new_issue_from_open_issue.png
+++ b/doc/user/project/issues/img/new_issue_from_open_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png
index cddf36b7457..4b9535f6b15 100755..100644
--- a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png
+++ b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png
Binary files differ
diff --git a/doc/user/project/issues/img/new_issue_from_tracker_list.png b/doc/user/project/issues/img/new_issue_from_tracker_list.png
index 7e5413f0b7d..66793cb44fa 100755..100644
--- a/doc/user/project/issues/img/new_issue_from_tracker_list.png
+++ b/doc/user/project/issues/img/new_issue_from_tracker_list.png
Binary files differ
diff --git a/doc/user/project/issues/img/project_issues_list_view.png b/doc/user/project/issues/img/project_issues_list_view.png
index 2fcc9e8d9da..584a81aab8a 100644
--- a/doc/user/project/issues/img/project_issues_list_view.png
+++ b/doc/user/project/issues/img/project_issues_list_view.png
Binary files differ
diff --git a/doc/user/project/issues/img/sidebar_confidential_issue.png b/doc/user/project/issues/img/sidebar_confidential_issue.png
index d99a1ca756e..d99a1ca756e 100755..100644
--- a/doc/user/project/issues/img/sidebar_confidential_issue.png
+++ b/doc/user/project/issues/img/sidebar_confidential_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/sidebar_move_issue.png b/doc/user/project/issues/img/sidebar_move_issue.png
index 111f7861364..1e688cec894 100644
--- a/doc/user/project/issues/img/sidebar_move_issue.png
+++ b/doc/user/project/issues/img/sidebar_move_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/sidebar_not_confidential_issue.png b/doc/user/project/issues/img/sidebar_not_confidential_issue.png
index 2e6cbbc5b3a..2e6cbbc5b3a 100755..100644
--- a/doc/user/project/issues/img/sidebar_not_confidential_issue.png
+++ b/doc/user/project/issues/img/sidebar_not_confidential_issue.png
Binary files differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 0f187946a4a..3e81dcb78c6 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -90,6 +90,10 @@ Learn distinct ways to [close issues](closing_issues.md) in GitLab.
Read through the [documentation on moving issues](moving_issues.md).
+## Deleting issues
+
+Read through the [documentation on deleting issues](deleting_issues.md)
+
## Create a merge request from an issue
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request).
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index 074b2c19c43..66140f389af 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -167,6 +167,7 @@ Once you wrote your comment, you can either:
#### 18. New Merge Request
- Create a new merge request (with a new source branch named after the issue) in one action.
-The merge request will automatically close that issue as soon as merged.
+The merge request will automatically inherit the milestone and labels of the issue. The merge
+request will automatically close that issue as soon as merged.
- Optionally, you can just create a [new branch](../repository/web_editor.md#create-a-new-branch-from-an-issue)
named after that issue.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 8ec7adad172..21a2e1213ec 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**.
The first time you visit this page, you'll notice that there are no labels
created yet.
-![Generate new labels](img/labels_generate.png)
-
Creating a new label from scratch is as easy as pressing the **New label**
button. From there on you can choose the name, give it an optional description,
a color and you are set.
@@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label.
---
-## Default Labels
-
-It's possible to populate the labels for your project from a set of predefined labels.
-
-### Generate GitLab's predefined label set
+## Default labels
-![Generate new labels](img/labels_generate.png)
+The very first time you visit the labels area, it's gonna be empty. In that
+case, it's possible to populate the labels for your project from a set of
+predefined labels.
Click the link to 'Generate a default set of labels' and GitLab will
-generate a set of predefined labels for you. There are 8 default generated labels
-in total and you can see them in the screenshot below.
-
-![Default generated labels](img/labels_default.png)
+generate them for you. There are 8 default generated labels in total:
----
+- bug
+- confirmed
+- critical
+- discussion
+- documentation
+- enhancement
+- suggestion
+- support
## Labels Overview
@@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels
that are relevant to you. You’ll notice it’ll be much easier to focus on what’s
important.
-## Create a new label right from the issue tracker
-
-> Introduced in GitLab 8.6.
+## Create a new label when inside an issue
-There are times when you are already in the issue tracker searching for a
+There are times when you are already inside an issue searching to assign a
label, only to realize it doesn't exist. Instead of going to the **Labels**
page and being distracted from your original purpose, you can create new
labels on the fly.
-Select **Create new** from the labels dropdown list, provide a name, pick a
-color and hit **Create**.
+Expand the issue sidebar and select **Create new label** from the labels dropdown
+list. Provide a name, pick a color and hit **Create**. The new label will be
+ready to used right away!
-![Create new label on the fly](img/labels_new_label_on_the_fly_create.png)
![New label on the fly](img/labels_new_label_on_the_fly.png)
## Assigning labels to issues and merge requests
There are generally two ways to assign a label to an issue or merge request.
-You can assign a label when you first create or edit an issue or merge request.
-
-![Assign label in new issue](img/labels_assign_label_in_new_issue.png)
-
----
+The first one is to assign a label when you first create or edit an issue or
+merge request.
The second way is by using the right sidebar when inside an issue or merge
request. Expand it and hit **Edit** in the labels area. Start typing the name
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
index 64b94d81024..22ef11e4049 100644
--- a/doc/user/project/merge_requests/cherry_pick_changes.md
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -2,24 +2,19 @@
> [Introduced][ce-3514] in GitLab 8.7.
----
-
GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
-with introducing a **Cherry-pick** button in Merge Requests and commit details.
+with introducing a **Cherry-pick** button in merge requests and commit details.
-## Cherry-picking a Merge Request
+## Cherry-picking a merge request
-After the Merge Request has been merged, a **Cherry-pick** button will be available
-to cherry-pick the changes introduced by that Merge Request:
+After the merge request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that merge request.
![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
----
-
-You can cherry-pick the changes directly into the selected branch or you can opt to
-create a new Merge Request with the cherry-pick changes:
-
-![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+After you click that button, a modal will appear where you can choose to
+cherry-pick the changes directly into the selected branch or you can opt to
+create a new merge request with the cherry-pick changes
## Cherry-picking a Commit
@@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page:
![Cherry-pick commit](img/cherry_pick_changes_commit.png)
----
-
-Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
-directly into the target branch or create a new Merge Request to cherry-pick the
-changes:
-
-![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
-
----
+Similar to cherry-picking a merge request, you can opt to cherry-pick the changes
+directly into the target branch or create a new merge request to cherry-pick the
+changes.
Please note that when cherry-picking merge commits, the mainline will always be the
first parent. If you want to use a different mainline then you need to do that
diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md
new file mode 100644
index 00000000000..085170d9f03
--- /dev/null
+++ b/doc/user/project/merge_requests/fast_forward_merge.md
@@ -0,0 +1,35 @@
+# Fast-forward merge requests
+
+Retain a linear Git history and a way to accept merge requests without
+creating merge commits.
+
+## Overview
+
+When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge
+commits will be created and all merges are fast-forwarded, which means that
+merging is only allowed if the branch could be fast-forwarded.
+
+When a fast-forward merge is not possible, the user must rebase the branch manually.
+
+## Use cases
+
+Sometimes, a workflow policy might mandate a clean commit history without
+merge commits. In such cases, the fast-forward merge is the perfect candidate.
+
+## Enabling fast-forward merges
+
+1. Navigate to your project's **Settings** and search for the 'Merge method'
+1. Select the **Fast-forward merge** option
+1. Hit **Save changes** for the changes to take effect
+
+Now, when you visit the merge request page, you will be able to accept it
+**only if a fast-forward merge is possible**.
+
+![Fast forward merge request](img/ff_merge_mr.png)
+
+If the target branch is ahead of the source branch, you need to rebase the
+source branch locally before you will be able to do a fast-forward merge.
+
+![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png)
+
+[ffonly]: https://git-scm.com/docs/git-merge#git-merge---ff-only
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
index 5ab094ab367..7dc344f8cf6 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
deleted file mode 100644
index 42dcb9203ec..00000000000
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
index 71227747182..811b0998f85 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
deleted file mode 100644
index 604eb22f51c..00000000000
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png
deleted file mode 100644
index e612a39716e..00000000000
--- a/doc/user/project/merge_requests/img/commit_compare.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png
new file mode 100644
index 00000000000..241cc990343
--- /dev/null
+++ b/doc/user/project/merge_requests/img/ff_merge_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png b/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png
new file mode 100644
index 00000000000..fb412296efc
--- /dev/null
+++ b/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png
index 02a88d0112f..7d0756505db 100644
--- a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png
+++ b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request.png b/doc/user/project/merge_requests/img/merge_request.png
new file mode 100644
index 00000000000..f9ca6348953
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
index 33f5a4a7a02..d7f0535d3c5 100644
--- a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
deleted file mode 100644
index ef7b6dae553..00000000000
--- a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
deleted file mode 100644
index f6540c9dd33..00000000000
--- a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
index 33c58d2abff..3883fb4bc1c 100644
--- a/doc/user/project/merge_requests/img/versions.png
+++ b/doc/user/project/merge_requests/img/versions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png
index db978ea7b1d..f5bd85dc7c1 100644
--- a/doc/user/project/merge_requests/img/versions_compare.png
+++ b/doc/user/project/merge_requests/img/versions_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png
index 889a2d93e6c..cc70a5bf14b 100644
--- a/doc/user/project/merge_requests/img/versions_dropdown.png
+++ b/doc/user/project/merge_requests/img/versions_dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
index 047b0b4620f..0c492aca363 100644
--- a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
+++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
index 8bd206bc24a..e405879b28a 100644
--- a/doc/user/project/merge_requests/img/wip_mark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
index c0bfa6a35a2..d7f8c419945 100644
--- a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 26c6277d33a..6289fcf3c2b 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -3,6 +3,8 @@
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
+![Merge request view](img/merge_request.png)
+
## Overview
A Merge Request (**MR**) is the basis of GitLab as a code collaboration
@@ -23,12 +25,14 @@ With GitLab merge requests, you can:
- Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md)
- Add a time estimation and the time spent with that merge request with [Time Tracking](../../../workflow/time_tracking.html#time-tracking)
- [Resolve merge conflicts from the UI](#resolve-conflicts)
+- Enable [fast-forward merge requests](#fast-forward-merge-requests)
+- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
+
With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter)
-- Enable [fast-forward merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html) (available in GitLab Enterprise Edition Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter)
- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter)
- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter)
@@ -89,6 +93,22 @@ in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](cherry_pick_changes.md)
+## Semi-linear history merge requests
+
+A merge commit is created for every merge, but the branch is only merged if
+a fast-forward merge is possible. This ensures that if the merge request build
+succeeded, the target branch build will also succeed after merging.
+
+Navigate to a project's settings, select the **Merge commit with semi-linear
+history** option under **Merge Requests: Merge method** and save your changes.
+
+## Fast-forward merge requests
+
+If you prefer a linear Git history and a way to accept merge requests without
+creating merge commits, you can configure this on a per-project basis.
+
+[Read more about fast-forward merge requests.](fast_forward_merge.md)
+
## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
@@ -254,4 +274,4 @@ git checkout origin/merge-requests/1
```
[protected branches]: ../protected_branches.md
-[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition" \ No newline at end of file
+[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition"
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
index 5ead9f4177f..8cf8a59dbfe 100644
--- a/doc/user/project/merge_requests/revert_changes.md
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -2,51 +2,39 @@
> [Introduced][ce-1990] in GitLab 8.5.
----
-
GitLab implements Git's powerful feature to [revert any commit][git-revert]
-with introducing a **Revert** button in Merge Requests and commit details.
+with introducing a **Revert** button in merge requests and commit details.
## Reverting a Merge Request
-_**Note:** The **Revert** button will only be available for Merge Requests
-created since GitLab 8.5. However, you can still revert a Merge Request
-by reverting the merge commit from the list of Commits page._
+NOTE: **Note:**
+The **Revert** button will only be available for merge requests
+created since GitLab 8.5. However, you can still revert a merge request
+by reverting the merge commit from the list of Commits page.
After the Merge Request has been merged, a **Revert** button will be available
-to revert the changes introduced by that Merge Request:
-
-![Revert Merge Request](img/revert_changes_mr.png)
-
----
-
-You can revert the changes directly into the selected branch or you can opt to
-create a new Merge Request with the revert changes:
+to revert the changes introduced by that merge request.
-![Revert Merge Request modal](img/revert_changes_mr_modal.png)
+![Revert Merge Request](img/cherry_pick_changes_mr.png)
----
+After you click that button, a modal will appear where you can choose to
+revert the changes directly into the selected branch or you can opt to
+create a new merge request with the revert changes.
-After the Merge Request has been reverted, the **Revert** button will not be
+After the merge request has been reverted, the **Revert** button will not be
available anymore.
## Reverting a Commit
You can revert a Commit from the Commit details page:
-![Revert commit](img/revert_changes_commit.png)
-
----
-
-Similar to reverting a Merge Request, you can opt to revert the changes
-directly into the target branch or create a new Merge Request to revert the
-changes:
-
-![Revert commit modal](img/revert_changes_commit_modal.png)
+![Revert commit](img/cherry_pick_changes_commit.png)
----
+Similar to reverting a merge request, you can opt to revert the changes
+directly into the target branch or create a new merge request to revert the
+changes.
-After the Commit has been reverted, the **Revert** button will not be available
+After the commit has been reverted, the **Revert** button will not be available
anymore.
Please note that when reverting merge commits, the mainline will always be the
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 46fa4378fe7..453e10184f0 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a
-[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/).
+[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under.
@@ -73,6 +73,8 @@ Pages wildcard domain. This guide is valid for any GitLab instance,
you just need to replace Pages wildcard domain on GitLab.com
(`*.gitlab.io`) with your own.
+Learn more about [namespaces](../../group/index.md#namespaces).
+
### Practical examples
#### Project Websites
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 53fd1786cfa..0096f8507d2 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,9 +1,14 @@
+---
+last_updated: 2017-09-28
+---
+
# GitLab Pages from A to Z: Part 3
-> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide ||
+> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles)**: user guide ||
> **Level**: beginner ||
> **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
-> **Publication date:** 2017/02/22
+> **Publication date:** 2017-02-22 ||
+> **Last updated**: 2017-09-28
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
@@ -16,6 +21,21 @@ As described in the previous part of this series, setting up GitLab Pages with c
These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`.
+### Adding your custom domain to GitLab Pages
+
+To use one or more custom domain with your Pages site, there are two things
+you should consider first, which we'll cover in this guide:
+
+1. Either if you're adding a **root domain** or a **subdomain**, for which
+you'll need to set up [DNS records](#dns-records)
+1. Whether you want to add an [SSL/TLS certificate](#ssl-tls-certificates) or not
+
+To finish the association, you need to [add your domain to your project's Pages settings](#add-your-custom-domain-to-gitlab-pages-settings).
+
+Let's start from the beginning with [DNS records](#dns-records).
+If you already know how they work and want to skip the introduction to DNS,
+you may be interested in skipping it until the [TL;DR](#tl-dr) section below.
+
### DNS Records
A Domain Name System (DNS) web service routes visitors to websites
@@ -99,6 +119,29 @@ domain. E.g., **do not** point your `subdomain.domain.com` to
`namespace.gitlab.io.` or `namespace.gitlab.io/`.
> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`.
+### Add your custom domain to GitLab Pages settings
+
+Once you've set the DNS record, you'll need navigate to your project's
+**Setting > Pages** and click **+ New domain** to add your custom domain to
+GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssl-tls-certificates)
+to make your website accessible under HTTPS or leave it blank. If don't add a certificate,
+your site will be accessible only via HTTP:
+
+![Add new domain](img/add_certificate_to_pages.png)
+
+You can add more than one alias (custom domains and subdomains) to the same project.
+An alias can be understood as having many doors leading to the same room.
+
+All the aliases you've set to your site will be listed on **Setting > Pages**.
+From that page, you can view, add, and remove them.
+
+Note that [DNS propagation may take some time (up to 24h)](http://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes),
+although it's usually a matter of minutes to complete. Until it does, visit attempts
+to your domain will respond with a 404.
+
+Read through the [general documentation on GitLab Pages](introduction.md#add-a-custom-domain-to-your-pages-website) to learn more about adding
+custom domains to GitLab Pages sites.
+
### SSL/TLS Certificates
Every GitLab Pages project on GitLab.com will be available under
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 9ecf7a3a8e7..4fcdfa7b281 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -28,7 +28,8 @@ In general there are two types of pages one might create:
- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
In GitLab, usernames and groupnames are unique and we often refer to them
-as namespaces. There can be only one namespace in a GitLab instance. Below you
+as [namespaces](../../group/index.md#namespaces). There can be only one namespace
+in a GitLab instance. Below you
can see the connection between the type of GitLab Pages, what the project name
that is created on GitLab looks like and the website URL it will be ultimately
be served on.
@@ -98,6 +99,9 @@ The steps to create a project page for a user or a group are identical:
A user's project will be served under `http(s)://username.example.io/projectname`
whereas a group's project under `http(s)://groupname.example.io/projectname`.
+For practical examples for group and project Pages, read through the guide
+[GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#practical-examples).
+
## Quick Start
Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on
@@ -111,6 +115,9 @@ The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
gives you absolute control over the build process. You can actually watch your
website being built live by following the CI job traces.
+For a simplified user guide on setting up GitLab CI/CD for Pages, read through
+the article [GitLab Pages from A to Z: Part 4 - Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md#creating-and-tweaking-gitlab-ci-yml-for-gitlab-pages)
+
> **Note:**
> Before reading this section, make sure you familiarize yourself with GitLab CI
> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
@@ -311,6 +318,9 @@ Visit the GitLab Pages group for a full list of example projects:
### Add a custom domain to your Pages website
+For a complete guide on Pages domains, read through the article
+[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates)
+
If this setting is enabled by your GitLab administrator, you should be able to
see the **New Domain** button when visiting your project's settings through the
gear icon in the top right and then navigating to **Pages**.
@@ -349,6 +359,9 @@ private key when adding a new domain.
![Pages upload cert](img/pages_upload_cert.png)
+For a complete guide on Pages domains, read through the article
+[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates)
+
### Custom error codes pages
You can provide your own 403 and 404 error pages by creating the `403.html` and
@@ -387,6 +400,8 @@ If you are using GitLab.com to host your website, then:
The rest of the guide still applies.
+See also: [GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain).
+
## Limitations
When using Pages under the general domain of a GitLab instance (`*.example.io`),
diff --git a/doc/user/project/pipelines/img/job_artifacts_browser.png b/doc/user/project/pipelines/img/job_artifacts_browser.png
index 145fe156bbb..d3d8de5ac60 100644
--- a/doc/user/project/pipelines/img/job_artifacts_browser.png
+++ b/doc/user/project/pipelines/img/job_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 4e93e680fd2..9ef6f9185c9 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -50,6 +50,10 @@ For more examples on artifacts, follow the [artifacts reference in
With GitLab 9.2, PDFs, images, videos and other formats can be previewed
directly in the job artifacts browser without the need to download them.
+>**Note:**
+With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
+directly in a new tab without the need to download them.
+
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
the **Keep** button appears only if you have set an [expiry date] to the
@@ -64,7 +68,8 @@ archive. If your artifacts contained directories, then you are also able to
browse inside them.
Below you can see how browsing looks like. In this case we have browsed inside
-the archive and at this point there is one directory and one HTML file.
+the archive and at this point there is one directory, a couple files, and
+one HTML file that you can view directly online (opens in a new tab).
![Job artifacts browser](img/job_artifacts_browser.png)
@@ -158,3 +163,4 @@ information in the UI.
[expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in
+[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index dbc1305101f..56f58fd755a 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -1,7 +1,7 @@
# Pipelines settings
To reach the pipelines settings navigate to your project's
-**Settings âž” Pipelines**.
+**Settings âž” CI/CD**.
The following settings can be configured per project.
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 1948627ee79..e1d3aebb8b3 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -1,5 +1,32 @@
# Branches
+Read through GiLab's branching documentation:
+
+- [Create a branch](../web_editor.md#create-a-new-branch)
+- [Default branch](#default-branch)
+- [Protected branches](../../protected_branches.md#protected-branches)
+- [Delete merged branches](#delete-merged-branches)
+
+See also:
+
+- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow): use the best of GitLab for your branching strategies
+- [Getting started with Git](../../../../topics/git/index.md) and GitLab
+
+## Default branch
+
+When you create a new [project](../../index.md), GitLab sets `master` as the default
+branch for your project. You can choose another branch to be your project's
+default under your project's **Settings > General**.
+
+The default branch is the branched affected by the
+[issue closing pattern](../../issues/automatic_issue_closing.md),
+which means that _an issue will be closed when a merge request is merged to
+the **default branch**_.
+
+The default branch is also protected against accidental deletion. Read through
+the documentation on [protected branches](../../protected_branches.md#protected-branches)
+to learn more.
+
## Delete merged branches
> [Introduced][ce-6449] in GitLab 8.14.
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index 20aadb8f7ff..6b9976d133c 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -26,7 +26,7 @@ to be uploaded to GitLab. For a signature to be verified three conditions need
to be met:
1. The public key needs to be added your GitLab account
-1. One of the emails in the GPG key matches your **primary** email
+1. One of the emails in the GPG key matches a **verified** email address you use in GitLab
1. The committer's email matches the verified email from the gpg key
## Generating a GPG key
@@ -94,7 +94,7 @@ started:
```
1. Enter you real name, the email address to be associated with this key (should
- match the primary email address you use in GitLab) and an optional comment
+ match a verified email address you use in GitLab) and an optional comment
(press <kbd>Enter</kbd> to skip):
```
@@ -113,25 +113,25 @@ started:
1. Use the following command to list the private GPG key you just created:
```
- gpg --list-secret-keys mr@robot.sh
+ gpg --list-secret-keys --keyid-format LONG mr@robot.sh
```
Replace `mr@robot.sh` with the email address you entered above.
1. Copy the GPG key ID that starts with `sec`. In the following example, that's
- `0x30F2B65B9246B6CA`:
+ `30F2B65B9246B6CA`:
```
- sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
+ sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC]
D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
uid [ultimate] Mr. Robot <mr@robot.sh>
- ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
+ ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E]
```
1. Export the public key of that ID (replace your key ID from the previous step):
```
- gpg --armor --export 0x30F2B65B9246B6CA
+ gpg --armor --export 30F2B65B9246B6CA
```
1. Finally, copy the public key and [add it in your profile settings](#adding-a-gpg-key-to-your-account)
@@ -167,28 +167,28 @@ key to use.
1. Use the following command to list the private GPG key you just created:
```
- gpg --list-secret-keys mr@robot.sh
+ gpg --list-secret-keys --keyid-format LONG mr@robot.sh
```
Replace `mr@robot.sh` with the email address you entered above.
1. Copy the GPG key ID that starts with `sec`. In the following example, that's
- `0x30F2B65B9246B6CA`:
+ `30F2B65B9246B6CA`:
```
- sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
+ sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC]
D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
uid [ultimate] Mr. Robot <mr@robot.sh>
- ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
+ ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E]
```
1. Tell Git to use that key to sign the commits:
```
- git config --global user.signingkey 0x30F2B65B9246B6CA
+ git config --global user.signingkey 30F2B65B9246B6CA
```
- Replace `0x30F2B65B9246B6CA` with your GPG key ID.
+ Replace `30F2B65B9246B6CA` with your GPG key ID.
## Signing commits
diff --git a/doc/user/project/repository/img/compare_branches.png b/doc/user/project/repository/img/compare_branches.png
index 353bd72ef4e..d7ab587f030 100755..100644
--- a/doc/user/project/repository/img/compare_branches.png
+++ b/doc/user/project/repository/img/compare_branches.png
Binary files differ
diff --git a/doc/user/project/repository/img/contributors_graph.png b/doc/user/project/repository/img/contributors_graph.png
index c31da7aa1ff..c31da7aa1ff 100755..100644
--- a/doc/user/project/repository/img/contributors_graph.png
+++ b/doc/user/project/repository/img/contributors_graph.png
Binary files differ
diff --git a/doc/user/project/repository/img/repo_graph.png b/doc/user/project/repository/img/repo_graph.png
index 28da8ad9589..28da8ad9589 100755..100644
--- a/doc/user/project/repository/img/repo_graph.png
+++ b/doc/user/project/repository/img/repo_graph.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 235af83353d..9501db88f57 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -55,7 +55,7 @@ Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files
## Branches
-When you submit changes in a new branch, you create a new version
+When you submit changes in a new [branch](branches/index.md), you create a new version
of that project's file tree. Your branch contains all the changes
you are presenting, which are detected by Git line by line.
@@ -70,8 +70,9 @@ With [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/)
subscriptions, you can also request
[approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals) from your managers.
-To create, delete, and branches via GitLab's UI:
+To create, delete, and [branches](branches/index.md) via GitLab's UI:
+- [Default branches](branches/index.md#default-branch)
- [Create a branch](web_editor.md#create-a-new-branch)
- [Protected branches](../protected_branches.md#protected-branches)
- [Delete merged branches](branches/index.md#delete-merged-branches)
diff --git a/doc/user/project/settings/img/general_settings.png b/doc/user/project/settings/img/general_settings.png
new file mode 100644
index 00000000000..96f5b84871f
--- /dev/null
+++ b/doc/user/project/settings/img/general_settings.png
Binary files differ
diff --git a/doc/user/project/settings/img/merge_requests_settings.png b/doc/user/project/settings/img/merge_requests_settings.png
new file mode 100644
index 00000000000..b1f2dfa7376
--- /dev/null
+++ b/doc/user/project/settings/img/merge_requests_settings.png
Binary files differ
diff --git a/doc/user/project/settings/img/sharing_and_permissions_settings.png b/doc/user/project/settings/img/sharing_and_permissions_settings.png
new file mode 100644
index 00000000000..0f9cf9512af
--- /dev/null
+++ b/doc/user/project/settings/img/sharing_and_permissions_settings.png
Binary files differ
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
new file mode 100644
index 00000000000..a234a647b77
--- /dev/null
+++ b/doc/user/project/settings/index.md
@@ -0,0 +1,52 @@
+# Project settings
+
+You can adjust your [project](../index.md) settings by navigating
+to your project's homepage and clicking **Settings**.
+
+## General settings
+
+Adjust your project's path and name, description, avatar, [default branch](../repository/branches/index.md#default-branch), and tags:
+
+![general project settings](img/general_settings.png)
+
+### Sharing and permissions
+
+Set up your project's access, [visibility](../../../public_access/public_access.md), and enable [Container Registry](../container_registry.md) for your projects:
+
+![projects sharing permissions](img/sharing_and_permissions_settings.png)
+
+### Issue settings
+
+Add an [issue description template](../description_templates.md#description-templates) to your project, so that every new issue will start with a custom template.
+
+### Merge request settings
+
+Set up your project's merge request settings:
+
+- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
+- Merge request [description templates](../description_templates.md#description-templates).
+- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)_.
+- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
+- Enable [merge only when all discussions are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved).
+
+![project's merge request settings](img/merge_requests_settings.png)
+
+### Service Desk
+
+Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/).
+
+### Export project
+
+Learn how to [export a project](import_export.md#importing-the-project) in GitLab.
+
+### Advanced settings
+
+Here you can run housekeeping, archive, rename, transfer, or remove a project.
+
+#### Archiving a project
+
+>**Note:** Only Project Owners and Admin users have the permission to archive a project
+
+It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings.
+
+An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state.
diff --git a/doc/user/search/img/issues_any_assignee.png b/doc/user/search/img/issues_any_assignee.png
index 2f902bcc66c..2f902bcc66c 100755..100644
--- a/doc/user/search/img/issues_any_assignee.png
+++ b/doc/user/search/img/issues_any_assignee.png
Binary files differ
diff --git a/doc/user/search/img/issues_assigned_to_you.png b/doc/user/search/img/issues_assigned_to_you.png
index 36c670eedd5..36c670eedd5 100755..100644
--- a/doc/user/search/img/issues_assigned_to_you.png
+++ b/doc/user/search/img/issues_assigned_to_you.png
Binary files differ
diff --git a/doc/user/search/img/issues_author.png b/doc/user/search/img/issues_author.png
index 792f9746db6..792f9746db6 100755..100644
--- a/doc/user/search/img/issues_author.png
+++ b/doc/user/search/img/issues_author.png
Binary files differ
diff --git a/doc/user/search/img/issues_mrs_shortcut.png b/doc/user/search/img/issues_mrs_shortcut.png
index 6380b337b54..6380b337b54 100755..100644
--- a/doc/user/search/img/issues_mrs_shortcut.png
+++ b/doc/user/search/img/issues_mrs_shortcut.png
Binary files differ
diff --git a/doc/user/search/img/left_menu_bar.png b/doc/user/search/img/left_menu_bar.png
index d68a71cba8e..d68a71cba8e 100755..100644
--- a/doc/user/search/img/left_menu_bar.png
+++ b/doc/user/search/img/left_menu_bar.png
Binary files differ
diff --git a/doc/user/search/img/project_search.png b/doc/user/search/img/project_search.png
index 3150b40de29..3150b40de29 100755..100644
--- a/doc/user/search/img/project_search.png
+++ b/doc/user/search/img/project_search.png
Binary files differ
diff --git a/doc/user/search/img/search_issues_board.png b/doc/user/search/img/search_issues_board.png
index 84048ae6a02..84048ae6a02 100755..100644
--- a/doc/user/search/img/search_issues_board.png
+++ b/doc/user/search/img/search_issues_board.png
Binary files differ
diff --git a/doc/user/search/img/sort_projects.png b/doc/user/search/img/sort_projects.png
index 9bf2770b299..9bf2770b299 100755..100644
--- a/doc/user/search/img/sort_projects.png
+++ b/doc/user/search/img/sort_projects.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 21e96d8b11c..2b23c494dc4 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -31,8 +31,8 @@ on the search field on the top-right of your screen:
If you want to search for issues present in a specific project, navigate to
a project's **Issues** tab, and click on the field **Search or filter results...**. It will
-display a dropdown menu, from which you can add filters per author, assignee, milestone, label,
-and weight. When done, press **Enter** on your keyboard to filter the issues.
+display a dropdown menu, from which you can add filters per author, assignee, milestone,
+label, weight, and 'my-reaction' (based on your emoji votes). When done, press **Enter** on your keyboard to filter the issues.
![filter issues in a project](img/issue_search_filter.png)
@@ -63,8 +63,6 @@ the same way as you do for projects.
![filter issues in a group](img/group_issues_filter.png)
The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab.
-The search and filter UI currently uses dropdowns. In a future release, the same
-dynamic UI as above will be carried over here.
## Search history
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 673e08287a3..6b2aba47f54 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -36,6 +36,7 @@
- [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- [Merge requests versions](../user/project/merge_requests/versions.md)
- ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
+ - [Fast-forward merge requests](../user/project/merge_requests/fast_forward_merge.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature
deleted file mode 100644
index 21d7d6c3800..00000000000
--- a/features/profile/active_tab.feature
+++ /dev/null
@@ -1,29 +0,0 @@
-@profile
-Feature: Profile Active Tab
- Background:
- Given I sign in as a user
-
- Scenario: On Profile Home
- Given I visit profile page
- Then the active main tab should be Home
- And no other main tabs should be active
-
- Scenario: On Profile Account
- Given I visit profile account page
- Then the active main tab should be Account
- And no other main tabs should be active
-
- Scenario: On Profile SSH Keys
- Given I visit profile SSH keys page
- Then the active main tab should be SSH Keys
- And no other main tabs should be active
-
- Scenario: On Profile Preferences
- Given I visit profile preferences page
- Then the active main tab should be Preferences
- And no other main tabs should be active
-
- Scenario: On Profile Authentication log
- Given I visit Authentication log page
- Then the active main tab should be Authentication log
- And no other main tabs should be active
diff --git a/features/profile/emails.feature b/features/profile/emails.feature
deleted file mode 100644
index 19ed949f6ae..00000000000
--- a/features/profile/emails.feature
+++ /dev/null
@@ -1,26 +0,0 @@
-@profile
-Feature: Profile Emails
- Background:
- Given I sign in as a user
- And I visit profile emails page
-
- Scenario: I should see emails
- Then I should see my emails
-
- Scenario: Add new email
- Given I submit new email "my@email.com"
- Then I should see new email "my@email.com"
- And I should see my emails
-
- Scenario: Add duplicate email
- Given I submit duplicate email @user.email
- Then I should not have @user.email added
- And I should see my emails
-
- Scenario: Remove email
- Given I submit new email "my@email.com"
- Then I should see new email "my@email.com"
- And I should see my emails
- Then I click link "Remove" for "my@email.com"
- Then I should not see email "my@email.com"
- And I should see my emails
diff --git a/features/project/archived.feature b/features/project/archived.feature
deleted file mode 100644
index ad466f4f307..00000000000
--- a/features/project/archived.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-Feature: Project Archived
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I own project "Forum"
-
- Scenario: I should not see archived on project page of not-archive project
- And project "Forum" is archived
- And I visit project "Shop" page
- Then I should not see "Archived"
-
- Scenario: I should see archived on project page of archive project
- And project "Forum" is archived
- And I visit project "Forum" page
- Then I should see "Archived"
-
- Scenario: I archive project
- When project "Shop" has push event
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project archived
- Then I should see "Archived"
-
- Scenario: I unarchive project
- When project "Shop" has push event
- And project "Shop" is archived
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project unarchived
- Then I should not see "Archived"
diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature
deleted file mode 100644
index 3bf15b0cf87..00000000000
--- a/features/project/builds/summary.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-Feature: Project Builds Summary
- Background:
- Given I sign in as a user
- And I own a project
- And project has CI enabled
- And project has coverage enabled
- And project has a recent build
-
- @javascript
- Scenario: I browse build details page
- When I visit recent build details page
- Then I see details of a build
- And I see build trace
-
- @javascript
- Scenario: I browse project builds page
- When I visit project builds page
- Then I see coverage
- Then I see button to CI Lint
-
- @javascript
- Scenario: I erase a build
- Given recent build is successful
- And recent build has a build trace
- When I visit recent build details page
- And I click erase build button
- Then recent build has been erased
- And recent build summary does not have artifacts widget
- And recent build summary contains information saying that build has been erased
- And the build count cache is updated
diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature
deleted file mode 100644
index 7ee1d717d80..00000000000
--- a/features/project/commits/revert.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_commits
-Feature: Revert Commits
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- @javascript
- Scenario: I revert a commit
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see the revert commit notice
-
- @javascript
- Scenario: I revert a commit that was previously reverted
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- And I visit my project's commits page
- And I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see a revert error
-
- @javascript
- Scenario: I revert a commit in a new merge request
- Given I click on commit link
- 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/ff_merge_requests.feature b/features/project/ff_merge_requests.feature
new file mode 100644
index 00000000000..995e52f9332
--- /dev/null
+++ b/features/project/ff_merge_requests.feature
@@ -0,0 +1,24 @@
+Feature: Project Ff Merge Requests
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And merge request "Bug NS-05" is mergeable
+
+ @javascript
+ Scenario: I do ff-only merge for rebased branch
+ Given ff merge enabled
+ And merge request "Bug NS-05" is rebased
+ When I visit merge request page "Bug NS-05"
+ Then I should see ff-only merge button
+ When I accept this merge request
+ Then I should see merged request
+
+ @javascript
+ Scenario: I do ff-only merge for merged branch
+ Given ff merge enabled
+ And merge request "Bug NS-05" merged target
+ When I visit merge request page "Bug NS-05"
+ Then I should see ff-only merge button
+ When I accept this merge request
+ Then I should see merged request
diff --git a/features/project/group_links.feature b/features/project/group_links.feature
deleted file mode 100644
index 2657c4487ad..00000000000
--- a/features/project/group_links.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-Feature: Project Group Links
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" is shared with group "Ops"
- And project "Shop" is not shared with group "Market"
- And I visit project group links page
-
- Scenario: I should see list of groups
- Then I should see project already shared with group "Ops"
- Then I should see project is not shared with group "Market"
-
- @javascript
- Scenario: I share project with group
- When I select group "Market" for share
- Then I should see project is shared with group "Market"
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
deleted file mode 100644
index 1d7adfdd2c2..00000000000
--- a/features/project/issues/award_emoji.feature
+++ /dev/null
@@ -1,45 +0,0 @@
-@project_issues
-Feature: Award Emoji
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has issue "Bugfix"
- And I visit "Bugfix" issue page
-
- @javascript
- Scenario: I repeatedly add and remove thumbsup award in the issue
- Given I click the thumbsup award Emoji
- Then I have award added
- Given I click the thumbsup award Emoji
- Then I have no awards added
- Given I click the thumbsup award Emoji
- Then I have award added
-
- @javascript
- Scenario: I add and remove custom award in the issue
- Given I click to emoji-picker
- Then The emoji menu is visible
- And The search field is focused
- Then I click to emoji in the picker
- Then I have award added
- And I can remove it by clicking to icon
-
- @javascript
- Scenario: I can see the list of emoji categories
- Given I click to emoji-picker
- Then The emoji menu is visible
- And The search field is focused
- Then I can see the activity and food categories
-
- @javascript
- Scenario: I can search emoji
- Given I click to emoji-picker
- Then The emoji menu is visible
- And The search field is focused
- And I search "hand"
- Then I see search result for "hand"
-
- @javascript
- Scenario: I add award emoji using regular comment
- Given I leave comment with a single emoji
- Then I have new comment with emoji added
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 4f905674d8c..d6cfa524a3a 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -51,36 +51,34 @@ Feature: Project Issues
@javascript
Scenario: Visiting Issues after being sorted the list
Given I visit project "Shop" issues page
- And I sort the list by "Oldest updated"
+ And I sort the list by "Last updated"
And I visit my project's home page
And I visit project "Shop" issues page
- Then The list should be sorted by "Oldest updated"
+ Then The list should be sorted by "Last updated"
@javascript
Scenario: Visiting Merge Requests after being sorted the list
Given project "Shop" has a "Bugfix MR" merge request open
And I visit project "Shop" issues page
- And I sort the list by "Oldest updated"
+ And I sort the list by "Last updated"
And I visit project "Shop" merge requests page
- Then The list should be sorted by "Oldest updated"
+ Then The list should be sorted by "Last updated"
@javascript
Scenario: Visiting Merge Requests from a differente Project after sorting
Given project "Shop" has a "Bugfix MR" merge request open
And I visit project "Shop" merge requests page
- And I sort the list by "Oldest updated"
+ And I sort the list by "Last updated"
And I visit dashboard merge requests page
- Then The list should be sorted by "Oldest updated"
+ Then The list should be sorted by "Last updated"
@javascript
Scenario: Sort issues by upvotes/downvotes
Given project "Shop" have "Bugfix" open issue
And issue "Release 0.4" have 2 upvotes and 1 downvote
And issue "Tweet control" have 1 upvote and 2 downvotes
- And I sort the list by "Most popular"
- Then The list should be sorted by "Most popular"
- And I sort the list by "Least popular"
- Then The list should be sorted by "Least popular"
+ And I sort the list by "Popularity"
+ Then The list should be sorted by "Popularity"
# Markdown
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
deleted file mode 100644
index 0ebeded7fc5..00000000000
--- a/features/project/merge_requests.feature
+++ /dev/null
@@ -1,326 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Bug NS-04" open merge request
- And project "Shop" have "Feature NS-03" closed merge request
- And I visit project "Shop" merge requests page
-
- Scenario: I should see open merge requests
- Then I should see "Bug NS-04" in merge requests
- And I should not see "Feature NS-03" in merge requests
-
- Scenario: I should see CI status for merge requests
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- Given "Bug NS-05" has CI status
- When I visit project "Shop" merge requests page
- Then I should see merge request "Bug NS-05" with CI status
-
- Scenario: I should not see target branch name when it is project's default branch
- Then I should see "Bug NS-04" in merge requests
- And I should not see "master" branch
-
- Scenario: I should see target branch when it is different from default
- Given project "Shop" have "Bug NS-06" open merge request
- When I visit project "Shop" merge requests page
- Then I should see "feature_conflict" branch
-
- @javascript
- Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
- Given project "Shop" have "Bug NS-07" open merge request with rebased branch
- When I visit merge request page "Bug NS-07"
- Then I should not see the diverged commits count
-
- @javascript
- Scenario: I should see the numbers of diverged commits if the branch diverged from the target
- Given project "Shop" have "Bug NS-08" open merge request with diverged branch
- When I visit merge request page "Bug NS-08"
- Then I should see the diverged commits count
-
- @javascript
- Scenario: I should see rejected merge requests
- Given I click link "Closed"
- Then I should see "Feature NS-03" in merge requests
- And I should not see "Bug NS-04" in merge requests
-
- @javascript
- Scenario: I should see all merge requests
- Given I click link "All"
- Then I should see "Feature NS-03" in merge requests
- And I should see "Bug NS-04" in merge requests
-
- @javascript
- Scenario: I visit an open merge request page
- Given I click link "Bug NS-04"
- Then I should see merge request "Bug NS-04"
-
- @javascript
- Scenario: I visit a merged merge request page
- Given project "Shop" have "Feature NS-05" merged merge request
- And I click link "Merged"
- And I click link "Feature NS-05"
- Then I should see merge request "Feature NS-05"
-
- @javascript
- Scenario: I close merge request page
- Given I click link "Bug NS-04"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
-
- @javascript
- Scenario: I reopen merge request page
- Given I click link "Bug NS-04"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
- When I click link "Reopen"
- Then I should see reopened merge request "Bug NS-04"
-
- @javascript
- Scenario: I submit new unassigned merge request
- Given I click link "New Merge Request"
- And I submit new merge request "Wiki Feature"
- Then I should see merge request "Wiki Feature"
-
- @javascript
- Scenario: I comment on a merge request
- Given I visit merge request page "Bug NS-04"
- And I leave a comment like "XML attached"
- Then I should see comment "XML attached"
-
- @javascript
- Scenario: Visiting Merge Requests after being sorted the list
- Given I visit project "Shop" merge requests page
- And I sort the list by "Oldest updated"
- And I visit my project's home page
- And I visit project "Shop" merge requests page
- Then The list should be sorted by "Oldest updated"
-
- @javascript
- Scenario: Visiting Merge Requests from a different Project after sorting
- Given I visit project "Shop" merge requests page
- And I sort the list by "Oldest updated"
- And I visit dashboard merge requests page
- Then The list should be sorted by "Oldest updated"
-
- @javascript
- Scenario: Sort merge requests by upvotes/downvotes
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And project "Shop" have "Bug NS-06" open merge request
- And merge request "Bug NS-04" have 2 upvotes and 1 downvote
- And merge request "Bug NS-06" have 1 upvote and 2 downvotes
- And I sort the list by "Most popular"
- Then The list should be sorted by "Most popular"
- And I sort the list by "Least popular"
- Then The list should be sorted by "Least popular"
-
- @javascript
- Scenario: I comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on diff
- And I should see a badge of "1" next to the discussion link
-
- @javascript
- Scenario: I see a new comment on merge request diff from another user in the discussion tab
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And user "John Doe" leaves a comment like "Line is wrong" on diff
- Then I should see a discussion by user "John Doe" has started on diff
- And I should see a badge of "1" next to the discussion link
-
- @javascript
- Scenario: I edit a comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I change the comment "Line is wrong" to "Typo, please fix" on diff
- Then I should not see a diff comment saying "Line is wrong"
- And I should see a diff comment saying "Typo, please fix"
-
- @javascript
- Scenario: I delete a comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I should see a badge of "1" next to the discussion link
- And I delete the comment "Line is wrong" on diff
- And I click on the Discussion tab
- Then I should not see any discussion
- And I should see a badge of "0" next to the discussion link
-
- @javascript
- Scenario: I comment on a line of a commit in merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the commit in the merge request
- And I leave a comment like "Line is wrong" on diff in commit
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on commit diff
-
- @javascript
- Scenario: I comment on a commit in merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the commit in the merge request
- And I leave a comment on the diff page in commit
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on commit
-
- @javascript
- Scenario: I accept merge request with custom commit message
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And merge request "Bug NS-05" is mergeable
- And I visit merge request page "Bug NS-05"
- And merge request is mergeable
- Then I modify merge commit message
- And I accept this merge request
- Then I should see merged request
-
- # Markdown
-
- @javascript
- Scenario: Headers inside the description should have ids generated for them.
- When I visit merge request page "Bug NS-04"
- Then Header "Description header" should have correct id and link
-
- @javascript
- Scenario: Headers inside comments should not have ids generated for them.
- Given I visit merge request page "Bug NS-04"
- And I leave a comment with a header containing "Comment with a header"
- Then The comment with the header should not have an ID
-
- # Toggling inline comments
-
- @javascript
- Scenario: I hide comments on a merge request diff with comments in a single file
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- Then I should not see a comment like "Line is wrong here" in the third file
-
- @javascript
- Scenario: I show comments on a merge request diff with comments in a single file
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the third file
- Then I should see a comment like "Line is wrong" in the third file
-
- @javascript
- Scenario: I hide comments on a merge request diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- Then I should not see a comment like "Line is wrong here" in the third file
- And I should still see a comment like "Line is correct" in the second file
-
- @javascript
- Scenario: I show comments on a merge request diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- And I click link "Show inline discussion" of the third file
- Then I should see a comment like "Line is wrong" in the third file
- And I should still see a comment like "Line is correct" in the second file
-
- @javascript
- Scenario: I unfold diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I unfold diff
- Then I should see additional file lines
-
- @javascript
- Scenario: I unfold diff in Side-by-Side view
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I click Side-by-side Diff tab
- And I unfold diff
- Then I should see additional file lines
-
- @javascript
- Scenario: I show comments on a merge request side-by-side diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click Side-by-side Diff tab
- Then I should see comments on the side-by-side diff page
-
- @javascript
- Scenario: I view diffs on a merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- Then I should see the proper Inline and Side-by-side links
-
- # Description preview
-
- @javascript
- Scenario: I can't preview without text
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I haven't written any description text
- Then The Markdown preview tab should say there is nothing to do
-
- @javascript
- Scenario: I can preview with text
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I write a description like ":+1: Nice"
- Then The Markdown preview tab should display rendered Markdown
-
- @javascript
- Scenario: I preview a merge request description
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I preview a description text like "Bug fixed :smile:"
- Then I should see the Markdown preview
- And I should not see the Markdown text field
-
- @javascript
- Scenario: I can edit after preview
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I preview a description text like "Bug fixed :smile:"
- Then I should see the Markdown write tab
-
- @javascript
- Scenario: I can unsubscribe from merge request
- Given I visit merge request page "Bug NS-04"
- Then I should see that I am subscribed
- When I click button "Unsubscribe"
- Then I should see that I am unsubscribed
-
- @javascript
- Scenario: I can change the target branch
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- When I click the "Target branch" dropdown
- And I select a new target branch
- Then I should see new target branch changes
-
- @javascript
- Scenario: I can close merge request after commenting
- Given I visit merge request page "Bug NS-04"
- And I leave a comment like "XML attached"
- Then I should see comment "XML attached"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature
deleted file mode 100644
index 2ab1c19f452..00000000000
--- a/features/project/merge_requests/accept.feature
+++ /dev/null
@@ -1,28 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests Acceptance
- Background:
- Given There is an open Merge Request
- And I am signed in as a developer of the project
-
- @javascript
- Scenario: Accepting the Merge Request and removing the source branch
- Given I am on the Merge Request detail page
- When I check the "Remove source branch" option
- And I click on Accept Merge Request
- Then I should see merge request merged
- And I should not see the Remove Source Branch button
-
- @javascript
- Scenario: Accepting the Merge Request when URL has an anchor
- Given I am on the Merge Request detail with note anchor page
- When I check the "Remove source branch" option
- And I click on Accept Merge Request
- Then I should see merge request merged
- And I should not see the Remove Source Branch button
-
- @javascript
- Scenario: Accepting the Merge Request without removing the source branch
- Given I am on the Merge Request detail page
- When I click on Accept Merge Request
- Then I should see merge request merged
- And I should see the Remove Source Branch button
diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature
deleted file mode 100644
index aaac5fd7209..00000000000
--- a/features/project/merge_requests/revert.feature
+++ /dev/null
@@ -1,29 +0,0 @@
-@project_merge_requests
-Feature: Revert Merge Requests
- Background:
- Given There is an open Merge Request
- And I am signed in as a developer of the project
- And I am on the Merge Request detail page
- And I click on Accept Merge Request
- And I am on the Merge Request detail page
-
- @javascript
- Scenario: I revert a merge request
- Given I click on the revert button
- And I revert the changes directly
- Then I should see the revert merge request notice
-
- @javascript
- Scenario: I revert a merge request that was previously reverted
- Given I click on the revert button
- And I revert the changes directly
- And I am on the Merge Request detail page
- And I click on the revert button
- And I revert the changes directly
- Then I should see a revert error
-
- @javascript
- Scenario: I revert a merge request in a new merge request
- Given 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/milestone.feature b/features/project/milestone.feature
deleted file mode 100644
index 5e7b211fa27..00000000000
--- a/features/project/milestone.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-Feature: Project Milestone
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has labels: "bug", "feature", "enhancement"
- And project "Shop" has milestone "v2.2"
- And milestone has issue "Bugfix1" with labels: "bug", "feature"
- And milestone has issue "Bugfix2" with labels: "bug", "enhancement"
-
- @javascript
- Scenario: Listing labels from labels tab
- Given I visit project "Shop" milestones page
- And I click link "v2.2"
- And I click link "Labels"
- Then I should see the list of labels
- And I should see the labels "bug", "enhancement" and "feature"
diff --git a/features/project/service.feature b/features/project/service.feature
deleted file mode 100644
index 54f07ebca92..00000000000
--- a/features/project/service.feature
+++ /dev/null
@@ -1,87 +0,0 @@
-Feature: Project Services
- Background:
- Given I sign in as a user
- And I own project "Shop"
-
- Scenario: I should see project services
- When I visit project "Shop" services page
- Then I should see list of available services
-
- Scenario: Activate hipchat service
- When I visit project "Shop" services page
- And I click hipchat service link
- And I fill hipchat settings
- Then I should see the Hipchat success message
-
- Scenario: Activate hipchat service with custom server
- When I visit project "Shop" services page
- And I click hipchat service link
- And I fill hipchat settings with custom server
- Then I should see the Hipchat success message
-
- Scenario: Activate pivotaltracker service
- When I visit project "Shop" services page
- And I click pivotaltracker service link
- And I fill pivotaltracker settings
- Then I should see the Pivotaltracker success message
-
- Scenario: Activate Flowdock service
- When I visit project "Shop" services page
- And I click Flowdock service link
- And I fill Flowdock settings
- Then I should see the Flowdock success message
-
- Scenario: Activate Assembla service
- When I visit project "Shop" services page
- And I click Assembla service link
- And I fill Assembla settings
- Then I should see the Assembla success message
-
- Scenario: Activate Slack notifications service
- When I visit project "Shop" services page
- And I click Slack notifications service link
- And I fill Slack notifications settings
- Then I should see the Slack notifications success message
-
- Scenario: Activate Pushover service
- When I visit project "Shop" services page
- And I click Pushover service link
- And I fill Pushover settings
- Then I should see the Pushover success message
-
- Scenario: Activate email on push service
- When I visit project "Shop" services page
- And I click email on push service link
- And I fill email on push settings
- Then I should see the Emails on push success message
-
- Scenario: Activate JIRA service
- When I visit project "Shop" services page
- And I click jira service link
- And I fill jira settings
- Then I should see the JIRA success message
-
- Scenario: Activate Irker (IRC Gateway) service
- When I visit project "Shop" services page
- And I click Irker service link
- And I fill Irker settings
- Then I should see the Irker success message
-
- Scenario: Activate Atlassian Bamboo CI service
- When I visit project "Shop" services page
- And I click Atlassian Bamboo CI service link
- And I fill Atlassian Bamboo CI settings
- Then I should see the Bamboo success message
- And I should see empty field Change Password
-
- Scenario: Activate jetBrains TeamCity CI service
- When I visit project "Shop" services page
- And I click jetBrains TeamCity CI service link
- And I fill jetBrains TeamCity CI settings
- Then I should see the JetBrains success message
-
- Scenario: Activate Asana service
- When I visit project "Shop" services page
- And I click Asana service link
- And I fill Asana settings
- Then I should see the Asana success message
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
deleted file mode 100644
index cbbea237825..00000000000
--- a/features/project/shortcuts.feature
+++ /dev/null
@@ -1,63 +0,0 @@
-@dashboard
-Feature: Project Shortcuts
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- @javascript
- Scenario: Navigate to files tab
- Given I press "g" and "f"
- Then the active main tab should be Repository
- Then the active sub tab should be Files
-
- @javascript
- Scenario: Navigate to commits tab
- Given I visit my project's files page
- Given I press "g" and "c"
- Then the active main tab should be Repository
- Then the active sub tab should be Commits
-
- @javascript
- Scenario: Navigate to graph tab
- Given I press "g" and "n"
- Then the active sub tab should be Graph
- And the active main tab should be Repository
-
- @javascript
- Scenario: Navigate to repository charts tab
- Given I press "g" and "d"
- Then the active sub tab should be Charts
- And the active main tab should be Repository
-
- @javascript
- Scenario: Navigate to issues tab
- Given I press "g" and "i"
- Then the active main tab should be Issues
-
- @javascript
- Scenario: Navigate to merge requests tab
- Given I press "g" and "m"
- Then the active main tab should be Merge Requests
-
- @javascript
- Scenario: Navigate to snippets tab
- Given I press "g" and "s"
- Then the active main tab should be Snippets
-
- @javascript
- Scenario: Navigate to wiki tab
- Given I press "g" and "w"
- Then the active main tab should be Wiki
-
- @javascript
- Scenario: Navigate to project home
- Given I press "g" and "p"
- Then the active sub tab should be Home
- And the active main tab should be Project
-
- @javascript
- Scenario: Navigate to project feed
- Given I press "g" and "e"
- Then the active sub tab should be Activity
- And the active main tab should be Project
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
deleted file mode 100644
index 50bc4c93df3..00000000000
--- a/features/project/snippets.feature
+++ /dev/null
@@ -1,35 +0,0 @@
-Feature: Project Snippets
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Snippet one" snippet
- And project "Shop" have no "Snippet two" snippet
- And I visit project "Shop" snippets page
-
- Scenario: I should see snippets
- Given I visit project "Shop" snippets page
- 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"
- Then I should see snippet "Snippet three"
-
- @javascript
- Scenario: I comment on a snippet "Snippet one"
- Given I visit snippet page "Snippet one"
- And I leave a comment like "Good snippet!"
- Then I should see comment "Good snippet!"
-
- Scenario: I update "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Edit"
- And I submit new title "Snippet new title"
- Then I should see "Snippet new title"
-
- Scenario: I destroy "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Delete"
- Then I should not see "Snippet one" in snippets
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
deleted file mode 100644
index aed41924cd9..00000000000
--- a/features/project/team_management.feature
+++ /dev/null
@@ -1,26 +0,0 @@
-Feature: Project Team Management
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And gitlab user "Mike"
- And gitlab user "Dmitriy"
- And "Dmitriy" is "Shop" developer
- And I visit project "Shop" team page
-
- Scenario: Cancel team member
- Given I click cancel link for "Dmitriy"
- Then I visit project "Shop" team page
- And I should not see "Dmitriy" in team list
-
- Scenario: Import team from another project
- Given I own project "Website"
- And "Mike" is "Website" reporter
- When I visit project "Shop" team page
- And I click link "Import team from another project"
- And I submit "Website" project for import team
- Then I should see "Mike" in team list as "Reporter"
-
- Scenario: See all members of projects shared group
- Given I share project with group "OpenSource"
- And I visit project "Shop" team page
- Then I should see "Opensource" group user listing
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
deleted file mode 100644
index a04228de03b..00000000000
--- a/features/project/wiki.feature
+++ /dev/null
@@ -1,101 +0,0 @@
-Feature: Project Wiki
- Background:
- Given I sign in as a user
- And I own project "Shop"
- Given I visit project wiki page
-
- Scenario: Add new page
- Given I create the Wiki Home page
- Then I should see the newly created wiki page
-
- Scenario: Add new page with errors
- Given I create the Wiki Home page with no content
- Then I should see a "Content can't be blank" error message
- When I create the Wiki Home page
- Then I should see the newly created wiki page
-
- Scenario: Pressing Cancel while editing a brand new Wiki
- Given I click on the Cancel button
- Then I should be redirected back to the Edit Home Wiki page
-
- Scenario: Edit existing page
- Given I have an existing Wiki page
- And I browse to that Wiki page
- And I click on the Edit button
- And I change the content
- Then I should see the updated content
-
- Scenario: Pressing Cancel while editing an existing Wiki page
- Given I have an existing Wiki page
- And I browse to that Wiki page
- And I click on the Edit button
- And I click on the Cancel button
- Then I should be redirected back to that Wiki page
-
- Scenario: View page history
- Given I have an existing wiki page
- And That page has two revisions
- And I browse to that Wiki page
- And I click the History button
- Then I should see both revisions
-
- Scenario: Destroy Wiki page
- Given I have an existing wiki page
- And I browse to that Wiki page
- And I click on the Edit button
- And I click on the "Delete this page" button
- Then The page should be deleted
-
- Scenario: View all pages
- Given I have an existing wiki page
- And I browse to that Wiki page
- Then I should see the existing page in the pages list
-
- Scenario: File exists in wiki repo
- Given I have an existing Wiki page with images linked on page
- And I browse to wiki page with images
- And I click on existing image link
- Then I should see the image from wiki repo
-
- Scenario: Image in wiki repo shown on the page
- Given I have an existing Wiki page with images linked on page
- And I browse to wiki page with images
- Then Image should be shown on the page
-
- Scenario: File does not exist in wiki repo
- Given I have an existing Wiki page with images linked on page
- And I browse to wiki page with images
- And I click on image link
- Then I should see the new wiki page form
-
- @javascript
- Scenario: New Wiki page that has a path
- Given I create a New page with paths
- Then I should see non-escaped link in the pages list
-
- @javascript
- Scenario: Edit Wiki page that has a path
- Given I create a New page with paths
- And I edit the Wiki page with a path
- Then I should see a non-escaped path
- And I should see the Editing page
- And I change the content
- Then I should see the updated content
-
- @javascript
- Scenario: View the page history of a Wiki page that has a path
- Given I create a New page with paths
- And I view the page history of a Wiki page that has a path
- Then I should see a non-escaped path
- And I should see the page history
-
- @javascript
- Scenario: View an old page version of a Wiki page
- Given I create a New page with paths
- And I edit the Wiki page with a path
- Then I should see a non-escaped path
- And I should see the Editing page
- And I change the content
- Then I click on Page History
- And I should see the page history
- And I should see a link with a version ID
diff --git a/features/search.feature b/features/search.feature
deleted file mode 100644
index f894b6b84a1..00000000000
--- a/features/search.feature
+++ /dev/null
@@ -1,100 +0,0 @@
-@dashboard
-Feature: Search
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit dashboard search page
-
- Scenario: I should see project I am looking for
- Given I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I should see issues I am looking for
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see merge requests I am looking for
- And project has merge requests
- When I search for "Foo"
- When I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see milestones I am looking for
- And project has milestones
- When I search for "Foo"
- When I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project code I am looking for
- When I click project "Shop" link
- And I search for "rspec"
- Then I should see code results for project "Shop"
-
- @javascript
- Scenario: I should see project issues
- And project has issues
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project merge requests
- And project has merge requests
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project milestones
- And project has milestones
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see Wiki blobs
- And project has Wiki content
- When I click project "Shop" link
- And I search for "Wiki content"
- And I click "Wiki" link
- Then I should see "test_wiki" link in the search results
-
- Scenario: I logout and should see project I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I logout and should see issues I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- Scenario: I logout and should see project code I am looking for
- Given project "Shop" is public
- And I logout directly
- When I visit project "Shop" page
- And I search for "rspec" on project page
- Then I should see code results for project "Shop"
diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb
deleted file mode 100644
index 069d4e6a23d..00000000000
--- a/features/steps/profile/active_tab.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Home' do
- ensure_active_main_tab('Profile')
- end
-
- step 'the active main tab should be Account' do
- ensure_active_main_tab('Account')
- end
-
- step 'the active main tab should be SSH Keys' do
- ensure_active_main_tab('SSH Keys')
- end
-
- step 'the active main tab should be Preferences' do
- ensure_active_main_tab('Preferences')
- end
-
- step 'the active main tab should be Authentication log' do
- ensure_active_main_tab('Authentication log')
- end
-end
diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb
deleted file mode 100644
index 4f44f932a6d..00000000000
--- a/features/steps/profile/emails.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-class Spinach::Features::ProfileEmails < Spinach::FeatureSteps
- include SharedAuthentication
-
- step 'I visit profile emails page' do
- visit profile_emails_path
- end
-
- step 'I should see my emails' do
- expect(page).to have_content(@user.email)
- @user.emails.each do |email|
- expect(page).to have_content(email.email)
- end
- end
-
- step 'I submit new email "my@email.com"' do
- fill_in "email_email", with: "my@email.com"
- click_button "Add"
- end
-
- step 'I should see new email "my@email.com"' do
- email = @user.emails.find_by(email: "my@email.com")
- expect(email).not_to be_nil
- expect(page).to have_content("my@email.com")
- end
-
- step 'I should not see email "my@email.com"' do
- email = @user.emails.find_by(email: "my@email.com")
- expect(email).to be_nil
- expect(page).not_to have_content("my@email.com")
- end
-
- step 'I click link "Remove" for "my@email.com"' do
- # there should only be one remove button at this time
- click_link "Remove"
- # force these to reload as they have been cached
- @user.emails.reload
- end
-
- step 'I submit duplicate email @user.email' do
- fill_in "email_email", with: @user.email
- click_button "Add"
- end
-
- step 'I should not have @user.email added' do
- email = @user.emails.find_by(email: @user.email)
- expect(email).to be_nil
- end
-end
diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb
deleted file mode 100644
index e4847180be9..00000000000
--- a/features/steps/project/archived.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class Spinach::Features::ProjectArchived < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- When 'project "Forum" is archived' do
- project = Project.find_by(name: "Forum")
- project.update_attribute(:archived, true)
- end
-
- When 'project "Shop" is archived' do
- project = Project.find_by(name: "Shop")
- project.update_attribute(:archived, true)
- end
-
- When 'I visit project "Forum" page' do
- project = Project.find_by(name: "Forum")
- visit project_path(project)
- end
-
- step 'I should not see "Archived"' do
- expect(page).not_to have_content "Archived"
- end
-
- step 'I should see "Archived"' do
- expect(page).to have_content "Archived"
- end
-
- When 'I set project archived' do
- click_link "Archive"
- end
-
- When 'I set project unarchived' do
- click_link "Unarchive"
- end
-end
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
deleted file mode 100644
index 20a5c873ecd..00000000000
--- a/features/steps/project/builds/summary.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedBuilds
- include RepoHelpers
-
- step 'I see coverage' do
- page.within('td.coverage') do
- expect(page).to have_content "99.9%"
- end
- end
-
- step 'I see button to CI Lint' do
- page.within('.nav-controls') do
- ci_lint_tool_link = page.find_link('CI lint')
- expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path)
- end
- end
-
- step 'I click erase build button' do
- click_link 'Erase'
- end
-
- step 'recent build has been erased' do
- expect(@build).not_to have_trace
- expect(@build.artifacts_file.exists?).to be_falsy
- expect(@build.artifacts_metadata.exists?).to be_falsy
- end
-
- step 'recent build summary does not have artifacts widget' do
- expect(page).to have_no_css('.artifacts')
- end
-
- step 'recent build summary contains information saying that build has been erased' do
- page.within('.erased') do
- expect(page).to have_content 'Job has been erased'
- end
- end
-
- step 'the build count cache is updated' do
- expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all)
- end
-end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 305fff37c41..318e054e978 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -139,7 +139,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'The diff links to both the previous and current image' do
- links = page.all('.two-up span div a')
+ links = page.all('.file-actions a')
expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}}
expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}}
end
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
deleted file mode 100644
index ebfa7a878bb..00000000000
--- a/features/steps/project/commits/revert.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class Spinach::Features::RevertCommits < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedDiffNote
- include RepoHelpers
-
- step 'I click on commit link' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I click on the revert button' do
- find(".header-action-buttons .dropdown").click
- find("a[href='#modal-revert-commit']").click
- end
-
- step 'I revert the changes directly' do
- page.within('#modal-revert-commit') do
- uncheck 'create_merge_request'
- click_button 'Revert'
- end
- end
-
- step 'I should see the revert commit notice' do
- page.should have_content('The commit has been successfully reverted.')
- end
-
- step 'I should see a revert error' do
- page.should have_content('Sorry, we cannot revert this commit automatically.')
- end
-
- step 'I revert the changes in a new merge request' do
- page.within('#modal-revert-commit') do
- click_button 'Revert'
- end
- end
-
- step 'I should see the new merge request notice' do
- page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
- page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
- end
-end
diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb
new file mode 100644
index 00000000000..d68fe71e16e
--- /dev/null
+++ b/features/steps/project/ff_merge_requests.rb
@@ -0,0 +1,65 @@
+class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedProject
+ include SharedNote
+ include SharedPaths
+ include SharedMarkdown
+ include SharedDiffNote
+ include SharedUser
+ include WaitForRequests
+
+ step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
+ create(:merge_request_with_diffs,
+ title: "Bug NS-05",
+ source_project: project,
+ target_project: project,
+ author: project.users.first)
+ end
+
+ step 'I should see ff-only merge button' do
+ expect(page).to have_content "Fast-forward merge without a merge commit"
+ expect(page).to have_button 'Merge'
+ end
+
+ step 'merge request "Bug NS-05" is mergeable' do
+ merge_request.mark_as_mergeable
+ end
+
+ step 'I accept this merge request' do
+ page.within '.mr-state-widget' do
+ click_button "Merge"
+ end
+ end
+
+ step 'I should see merged request' do
+ page.within '.status-box' do
+ expect(page).to have_content "Merged"
+ wait_for_requests
+ end
+ end
+
+ step 'ff merge enabled' do
+ project = merge_request.target_project
+ project.merge_requests_ff_only_enabled = true
+ project.save!
+ end
+
+ step 'merge request "Bug NS-05" is rebased' do
+ merge_request.source_branch = 'flatten-dir'
+ merge_request.target_branch = 'improve/awesome'
+ merge_request.reload_diff
+ merge_request.save!
+ end
+
+ step 'merge request "Bug NS-05" merged target' do
+ merge_request.source_branch = 'merged-target'
+ merge_request.target_branch = 'improve/awesome'
+ merge_request.reload_diff
+ merge_request.save!
+ end
+
+ def merge_request
+ @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
+ end
+end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index f88738b4c61..60707f26aee 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I fork to my namespace' do
- page.within '.fork-namespaces' do
+ page.within '.fork-thumbnail-container' do
click_link current_user.name
end
end
@@ -58,13 +58,13 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I should see my fork on the list' do
page.within('.js-projects-list-holder') do
- project = @user.fork_of(@project)
+ project = @user.fork_of(@project.reload)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
end
end
step 'I make forked repo invalid' do
- project = @user.fork_of(@project)
+ project = @user.fork_of(@project.reload)
project.path = 'test-crappy-path'
project.save!
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 420ac8a695a..6781a906a94 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -5,6 +5,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include SharedPaths
include Select2Helper
include WaitForRequests
+ include ProjectForksHelper
step 'I am a member of project "Shop"' do
@project = ::Project.find_by(name: "Shop")
@@ -13,7 +14,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I have a project forked off of "Shop" called "Forked Shop"' do
- @forked_project = Projects::ForkService.new(@project, @user).execute
+ @forked_project = fork_project(@project, @user,
+ namespace: @user.namespace,
+ repository: true)
end
step 'I click link "New Merge Request"' do
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
deleted file mode 100644
index bbd284b4633..00000000000
--- a/features/steps/project/issues/award_emoji.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include Select2Helper
-
- step 'I visit "Bugfix" issue page' do
- visit project_issue_path(@project, @issue)
- end
-
- step 'I click the thumbsup award Emoji' do
- page.within '.awards' do
- thumbsup = page.first('.award-control')
- thumbsup.click
- thumbsup.hover
- end
- end
-
- step 'I click to emoji-picker' do
- page.within '.awards' do
- page.find('.js-add-award').click
- end
- end
-
- step 'I click to emoji in the picker' do
- page.within '.emoji-menu-content' do
- emoji_button = page.first('.js-emoji-btn')
- emoji_button.hover
- emoji_button.click
- end
- end
-
- step 'I can remove it by clicking to icon' do
- page.within '.awards' do
- expect do
- page.find('.js-emoji-btn.active').click
- wait_for_requests
- end.to change { page.all(".award-control.js-emoji-btn").size }.from(3).to(2)
- end
- end
-
- step 'I can see the activity and food categories' do
- page.within '.emoji-menu' do
- expect(page).not_to have_selector 'Activity'
- expect(page).not_to have_selector 'Food'
- end
- end
-
- step 'I have new comment with emoji added' do
- expect(page).to have_selector 'gl-emoji[data-name="smile"]'
- end
-
- step 'I have award added' do
- page.within '.awards' do
- expect(page).to have_selector '.js-emoji-btn'
- expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
- end
- end
-
- step 'I have no awards added' do
- page.within '.awards' do
- expect(page).to have_selector '.award-control.js-emoji-btn'
- expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
-
- # Check tooltip data
- page.all('.award-control.js-emoji-btn').each do |element|
- expect(element['title']).to eq("")
- end
-
- page.all('.award-control .js-counter').each do |element|
- expect(element).to have_content '0'
- end
- end
- end
-
- step 'project "Shop" has issue "Bugfix"' do
- @project = Project.find_by(name: 'Shop')
- @issue = create(:issue, title: 'Bugfix', project: project)
- end
-
- step 'I leave comment with a single emoji' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: ':smile:'
- click_button 'Comment'
- end
- end
-
- step 'I search "hand"' do
- fill_in 'emoji-menu-search', with: 'hand'
- end
-
- step 'I see search result for "hand"' do
- page.within '.emoji-menu-content' do
- expect(page).to have_selector '[data-name="raised_hand"]'
- end
- end
-
- step 'The emoji menu is visible' do
- page.find(".emoji-menu.is-visible")
- end
-
- step 'The search field is focused' do
- expect(page).to have_selector('.js-emoji-menu-search')
- expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
- end
-end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index b9460f5b534..2c3ef2efd52 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -223,7 +223,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
end
- step 'The list should be sorted by "Most popular"' do
+ step 'The list should be sorted by "Popularity"' do
page.within '.issues-list' do
page.within 'li.issue:nth-child(1)' do
expect(page).to have_content 'Release 0.4'
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
deleted file mode 100644
index 0d49a4ab90d..00000000000
--- a/features/steps/project/merge_requests.rb
+++ /dev/null
@@ -1,632 +0,0 @@
-class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedDiffNote
- include SharedUser
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I click link "New Merge Request"' do
- page.within '.nav-controls' do
- page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
- end
- end
-
- step 'I click link "Bug NS-04"' do
- click_link "Bug NS-04"
- end
-
- step 'I click link "Feature NS-05"' do
- click_link "Feature NS-05"
- end
-
- step 'I click link "All"' do
- find('.issues-state-filters [data-state="all"] span', text: 'All').click
- # Waits for load
- expect(find('.issues-state-filters > .active')).to have_content 'All'
- end
-
- step 'I click link "Merged"' do
- find('#state-merged').trigger('click')
- end
-
- step 'I click link "Closed"' do
- find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
- end
-
- step 'I should see merge request "Wiki Feature"' do
- page.within '.merge-request' do
- expect(page).to have_content "Wiki Feature"
- end
- wait_for_requests
- end
-
- step 'I should see closed merge request "Bug NS-04"' do
- expect(page).to have_content "Bug NS-04"
- expect(page).to have_content "Closed by"
- wait_for_requests
- end
-
- step 'I should see merge request "Bug NS-04"' do
- expect(page).to have_content "Bug NS-04"
- wait_for_requests
- end
-
- step 'I should see merge request "Feature NS-05"' do
- expect(page).to have_content "Feature NS-05"
- wait_for_requests
- end
-
- step 'I should not see "master" branch' do
- expect(find('.issuable-info')).not_to have_content "master"
- end
-
- step 'I should see "feature_conflict" branch' do
- expect(page).to have_content "feature_conflict"
- end
-
- step 'I should see "Bug NS-04" in merge requests' do
- expect(page).to have_content "Bug NS-04"
- end
-
- step 'I should see "Feature NS-03" in merge requests' do
- expect(page).to have_content "Feature NS-03"
- end
-
- step 'I should not see "Feature NS-03" in merge requests' do
- expect(page).not_to have_content "Feature NS-03"
- end
-
- step 'I should not see "Bug NS-04" in merge requests' do
- expect(page).not_to have_content "Bug NS-04"
- end
-
- step 'I should see that I am subscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
- end
-
- step 'I should see that I am unsubscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
- end
-
- step 'I click button "Unsubscribe"' do
- click_on "Unsubscribe"
- wait_for_requests
- end
-
- step 'I click link "Close"' do
- first(:css, '.close-mr-link').click
- end
-
- step 'I submit new merge request "Wiki Feature"' do
- find('.js-source-branch').click
- find('.dropdown-source-branch .dropdown-content a', text: 'fix').click
-
- find('.js-target-branch').click
- first('.dropdown-target-branch .dropdown-content a', text: 'feature').click
-
- click_button "Compare branches"
- fill_in "merge_request_title", with: "Wiki Feature"
- click_button "Submit merge request"
- end
-
- step 'project "Shop" have "Bug NS-04" open merge request' do
- create(:merge_request,
- title: "Bug NS-04",
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'merge-test',
- author: project.users.first,
- description: "# Description header"
- )
- end
-
- step 'project "Shop" have "Bug NS-06" open merge request' do
- create(:merge_request,
- title: "Bug NS-06",
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'feature_conflict',
- author: project.users.first,
- description: "# Description header"
- )
- end
-
- step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
- create(:merge_request_with_diffs,
- title: "Bug NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first,
- source_branch: 'merge-test')
- end
-
- step 'project "Shop" have "Feature NS-05" merged merge request' do
- create(:merged_merge_request,
- title: "Feature NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
- create(:merge_request, :rebased,
- title: "Bug NS-07",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do
- create(:merge_request, :diverged,
- title: "Bug NS-08",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Feature NS-03" closed merge request' do
- create(:closed_merge_request,
- title: "Feature NS-03",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Community" has "Bug CO-01" open merge request with diffs inside' do
- project = Project.find_by(name: "Community")
- create(:merge_request_with_diffs,
- title: "Bug CO-01",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
- merge_request = MergeRequest.find_by(title: 'Bug NS-04')
- create_list(:award_emoji, 2, awardable: merge_request)
- create(:award_emoji, :downvote, awardable: merge_request)
- end
-
- step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
- awardable = MergeRequest.find_by(title: 'Bug NS-06')
- create(:award_emoji, awardable: awardable)
- create_list(:award_emoji, 2, :downvote, awardable: awardable)
- end
-
- step 'The list should be sorted by "Least popular"' do
- page.within '.mr-list' do
- page.within 'li.merge-request:nth-child(1)' do
- expect(page).to have_content 'Bug NS-06'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.merge-request:nth-child(2)' do
- expect(page).to have_content 'Bug NS-04'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.merge-request:nth-child(3)' do
- expect(page).to have_content 'Bug NS-05'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- step 'The list should be sorted by "Most popular"' do
- page.within '.mr-list' do
- page.within 'li.merge-request:nth-child(1)' do
- expect(page).to have_content 'Bug NS-04'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.merge-request:nth-child(2)' do
- expect(page).to have_content 'Bug NS-06'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.merge-request:nth-child(3)' do
- expect(page).to have_content 'Bug NS-05'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- step 'I click on the Changes tab' do
- page.within '.merge-request-tabs' do
- click_link 'Changes'
- end
-
- # Waits for load
- expect(page).to have_css('.tab-content #diffs.active')
- end
-
- step 'I should see the proper Inline and Side-by-side links' do
- expect(page).to have_css('#parallel-diff-btn', count: 1)
- expect(page).to have_css('#inline-diff-btn', count: 1)
- end
-
- step 'I switch to the merge request\'s comments tab' do
- visit project_merge_request_path(project, merge_request)
- end
-
- step 'I click on the commit in the merge request' do
- page.within '.merge-request-tabs' do
- click_link 'Commits'
- end
-
- page.within '.commits' do
- click_link Commit.truncate_sha(sample_commit.id)
- end
- end
-
- step 'I leave a comment on the diff page' do
- init_diff_note
- leave_comment "One comment to rule them all"
- end
-
- step 'I leave a comment on the diff page in commit' do
- click_diff_line(sample_commit.line_code)
- leave_comment "One comment to rule them all"
- end
-
- step 'I leave a comment like "Line is wrong" on diff' do
- init_diff_note
- leave_comment "Line is wrong"
- end
-
- step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do
- mr = MergeRequest.find_by(title: "Bug NS-05")
- create(:diff_note_on_merge_request, project: project,
- noteable: mr,
- author: user_exists("John Doe"),
- note: 'Line is wrong')
- end
-
- step 'I leave a comment like "Line is wrong" on diff in commit' do
- click_diff_line(sample_commit.line_code)
- leave_comment "Line is wrong"
- end
-
- step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do
- page.within('.diff-file:nth-of-type(5) .note') do
- find('.js-note-edit').click
-
- page.within('.current-note-edit-form', visible: true) do
- fill_in 'note_note', with: 'Typo, please fix'
- click_button 'Save comment'
- end
-
- expect(page).not_to have_button 'Save comment', disabled: true, visible: true
- end
- end
-
- step 'I should not see a diff comment saying "Line is wrong"' do
- page.within('.diff-file:nth-of-type(5) .note') do
- expect(page).not_to have_visible_content 'Line is wrong'
- end
- end
-
- step 'I should see a diff comment saying "Typo, please fix"' do
- page.within('.diff-file:nth-of-type(5) .note') do
- expect(page).to have_visible_content 'Typo, please fix'
- end
- end
-
- step 'I delete the comment "Line is wrong" on diff' do
- page.within('.diff-file:nth-of-type(5) .note') do
- find('.more-actions').click
- find('.more-actions .dropdown-menu li', match: :first)
-
- find('.js-note-delete').click
- end
- end
-
- step 'I click on the Discussion tab' do
- page.within '.merge-request-tabs' do
- find('.notes-tab').trigger('click')
- end
-
- # Waits for load
- expect(page).to have_css('.tab-content #notes.active')
- end
-
- step 'I should not see any discussion' do
- expect(page).not_to have_css('.notes .discussion')
- end
-
- step 'I should see a discussion has started on diff' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- end
- end
-
- step 'I should see a discussion by user "John Doe" has started on diff' do
- # Trigger a refresh of notes
- execute_script("$(document).trigger('visibilitychange');")
- wait_for_requests
- page.within(".notes .discussion") do
- page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- end
- end
-
- step 'I should see a badge of "1" next to the discussion link' do
- expect_discussion_badge_to_have_counter("1")
- wait_for_requests
- end
-
- step 'I should see a badge of "0" next to the discussion link' do
- expect_discussion_badge_to_have_counter("0")
- wait_for_requests
- end
-
- step 'I should see a discussion has started on commit diff' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- wait_for_requests
- end
- end
-
- step 'I should see a discussion has started on commit' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
- page.should have_content "One comment to rule them all"
- wait_for_requests
- end
- end
-
- step 'merge request is mergeable' do
- expect(page).to have_button 'Merge'
- end
-
- step 'I modify merge commit message' do
- click_button "Modify commit message"
- fill_in 'Commit message', with: 'wow such merge'
- end
-
- step 'merge request "Bug NS-05" is mergeable' do
- merge_request.mark_as_mergeable
- end
-
- step 'I accept this merge request' do
- page.within '.mr-state-widget' do
- click_button "Merge"
- end
- end
-
- step 'I should see merged request' do
- page.within '.status-box' do
- expect(page).to have_content "Merged"
- wait_for_requests
- end
- end
-
- step 'I click link "Reopen"' do
- first(:css, '.reopen-mr-link').trigger('click')
- end
-
- step 'I should see reopened merge request "Bug NS-04"' do
- page.within '.status-box' do
- expect(page).to have_content "Open"
- end
- wait_for_requests
- end
-
- step 'I click link "Hide inline discussion" of the third file' do
- page.within '.files>div:nth-child(3)' do
- find('.js-toggle-diff-comments').trigger('click')
- end
- end
-
- step 'I click link "Show inline discussion" of the third file' do
- page.within '.files>div:nth-child(3)' do
- find('.js-toggle-diff-comments').trigger('click')
- end
- end
-
- step 'I should not see a comment like "Line is wrong" in the third file' do
- page.within '.files>div:nth-child(3)' do
- expect(page).not_to have_visible_content "Line is wrong"
- end
- end
-
- step 'I should see a comment like "Line is wrong" in the third file' do
- page.within '.files>div:nth-child(3) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is wrong"
- wait_for_requests
- end
- end
-
- step 'I should not see a comment like "Line is wrong here" in the third file' do
- page.within '.files>div:nth-child(3)' do
- expect(page).not_to have_visible_content "Line is wrong here"
- end
- end
-
- step 'I should see a comment like "Line is wrong here" in the third file' do
- page.within '.files>div:nth-child(3) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is wrong here"
- end
- end
-
- step 'I leave a comment like "Line is correct" on line 12 of the second file' do
- init_diff_note_first_file
-
- page.within(".js-discussion-note-form") do
- fill_in "note_note", with: "Line is correct"
- click_button "Comment"
- end
-
- wait_for_requests
-
- page.within ".files>div:nth-child(2) .note-body > .note-text" do
- expect(page).to have_content "Line is correct"
- end
- end
-
- step 'I leave a comment like "Line is wrong" on line 39 of the third file' do
- init_diff_note_second_file
-
- page.within(".js-discussion-note-form") do
- fill_in "note_note", with: "Line is wrong on here"
- click_button "Comment"
- end
-
- wait_for_requests
- end
-
- step 'I should still see a comment like "Line is correct" in the second file' do
- page.within '.files>div:nth-child(2) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is correct"
- end
- end
-
- step 'I unfold diff' do
- expect(page).to have_css('.js-unfold')
-
- first('.js-unfold').click
- end
-
- step 'I should see additional file lines' do
- expect(first('.text-file')).to have_content('.bundle')
- end
-
- step 'I click Side-by-side Diff tab' do
- find('a', text: 'Side-by-side').trigger('click')
-
- # Waits for load
- expect(page).to have_css('.parallel')
- end
-
- step 'I should see comments on the side-by-side diff page' do
- page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
- expect(page).to have_visible_content "Line is correct"
- wait_for_requests
- end
- end
-
- step 'I fill in merge request search with "Fe"' do
- fill_in 'issuable_search', with: "Fe"
- page.within '.merge-requests-holder' do
- find('.merge-request')
- end
- end
-
- step 'I click the "Target branch" dropdown' do
- expect(page).to have_content('Target branch')
- first('.target_branch').click
- end
-
- step 'I select a new target branch' do
- select "feature", from: "merge_request_target_branch"
- click_button 'Save'
- end
-
- step 'I should see new target branch changes' do
- expect(page).to have_content 'Request to merge fix into feature'
- expect(page).to have_content 'changed target branch from merge-test to feature'
- wait_for_requests
- end
-
- step 'I click on "Email Patches"' do
- click_link "Email Patches"
- end
-
- step 'I click on "Plain Diff"' do
- click_link "Plain Diff"
- end
-
- step 'I should see a patch diff' do
- expect(page).to have_content('diff --git')
- end
-
- step '"Bug NS-05" has CI status' do
- project = merge_request.source_project
- project.enable_ci
-
- pipeline =
- create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
-
- create :ci_build, pipeline: pipeline
- end
-
- step 'I should see merge request "Bug NS-05" with CI status' do
- page.within ".mr-list" do
- expect(page).to have_link "Pipeline: pending"
- end
- end
-
- step 'I should see the diverged commits count' do
- page.within ".mr-source-target" do
- expect(page).to have_content /([0-9]+ commits behind)/
- end
-
- wait_for_requests
- end
-
- step 'I should not see the diverged commits count' do
- page.within ".mr-source-target" do
- expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
- end
-
- wait_for_requests
- end
-
- def merge_request
- @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
- end
-
- def init_diff_note
- click_diff_line(sample_commit.line_code)
- end
-
- def leave_comment(message)
- page.within(".js-discussion-note-form", visible: true) do
- fill_in "note_note", with: message
- click_button "Comment"
- end
-
- wait_for_requests
-
- page.within(".notes_holder", visible: true) do
- expect(page).to have_content message
- end
- end
-
- def init_diff_note_first_file
- click_diff_line(sample_compare.changes[0][:line_code])
- end
-
- def init_diff_note_second_file
- click_diff_line(sample_compare.changes[1][:line_code])
- end
-
- def have_visible_content(text)
- have_css("*", text: text, visible: true)
- end
-
- def expect_discussion_badge_to_have_counter(value)
- page.within(".notes-tab .badge") do
- page.should have_content value
- end
- end
-end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
deleted file mode 100644
index 3c640e3512a..00000000000
--- a/features/steps/project/merge_requests/acceptance.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
- include LoginHelpers
- include WaitForRequests
-
- step 'I am on the Merge Request detail page' do
- visit merge_request_path(@merge_request)
- end
-
- step 'I am on the Merge Request detail with note anchor page' do
- visit merge_request_path(@merge_request, anchor: 'note_123')
- end
-
- step 'I uncheck the "Remove source branch" option' do
- uncheck('Remove source branch')
- end
-
- step 'I check the "Remove source branch" option' do
- check('Remove source branch')
- end
-
- step 'I click on Accept Merge Request' do
- click_button('Merge')
- end
-
- step 'I should see the Remove Source Branch button' do
- expect(page).to have_selector('.js-remove-branch-button')
-
- # Wait for View Resource requests to complete so they don't blow up if they are
- # only handled after `DatabaseCleaner` has already run
- wait_for_requests
- end
-
- step 'I should not see the Remove Source Branch button' do
- expect(page).not_to have_selector('.js-remove-branch-button')
-
- # Wait for View Resource requests to complete so they don't blow up if they are
- # only handled after `DatabaseCleaner` has already run
- wait_for_requests
- end
-
- step 'There is an open Merge Request' do
- @user = create(:user)
- @project = create(:project, :public, :repository)
- @project_member = create(:project_member, :developer, user: @user, project: @project)
- @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
- end
-
- step 'I am signed in as a developer of the project' do
- sign_in(@user)
- end
-
- step 'I should see merge request merged' do
- expect(page).to have_content('The changes were merged into')
- end
-end
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
deleted file mode 100644
index 25ccf5ab180..00000000000
--- a/features/steps/project/merge_requests/revert.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
- include LoginHelpers
- include WaitForRequests
-
- step 'I click on the revert button' do
- find("a[href='#modal-revert-commit']").click
- end
-
- step 'I revert the changes directly' do
- page.within('#modal-revert-commit') do
- uncheck 'create_merge_request'
- click_button 'Revert'
- end
- end
-
- step 'I should see the revert merge request notice' do
- page.should have_content('The merge request has been successfully reverted.')
- wait_for_requests
- end
-
- step 'I should not see the revert button' do
- expect(page).not_to have_selector(:xpath, "a[href='#modal-revert-commit']")
- end
-
- step 'I am on the Merge Request detail page' do
- visit merge_request_path(@merge_request)
- end
-
- step 'I click on Accept Merge Request' do
- click_button('Merge')
- end
-
- step 'I am signed in as a developer of the project' do
- @user = create(:user) { |u| @project.add_developer(u) }
- sign_in(@user)
- end
-
- step 'There is an open Merge Request' do
- @merge_request = create(:merge_request, :with_diffs, :simple)
- @project = @merge_request.source_project
- end
-
- step 'I should see a revert error' do
- page.should have_content('Sorry, we cannot revert this merge request automatically.')
- end
-
- step 'I revert the changes in a new merge request' do
- page.within('#modal-revert-commit') do
- click_button 'Revert'
- end
- end
-
- step 'I should see the new merge request notice' do
- page.should have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
- end
-end
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
deleted file mode 100644
index 47ee31786a6..00000000000
--- a/features/steps/project/project_group_links.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include Select2Helper
-
- step 'I should see project already shared with group "Ops"' do
- page.within '.project-members-groups' do
- expect(page).to have_content "Ops"
- end
- end
-
- step 'I should see project is not shared with group "Market"' do
- page.within '.project-members-groups' do
- expect(page).not_to have_content "Market"
- end
- end
-
- step 'I select group "Market" for share' do
- click_link 'Share with group'
- group = Group.find_by(path: 'market')
- select2(group.id, from: "#link_group_id")
- select "Master", from: 'link_group_access'
- click_button "Share"
- end
-
- step 'I should see project is shared with group "Market"' do
- page.within '.project-members-groups' do
- expect(page).to have_content "Market"
- end
- end
-
- step 'project "Shop" is shared with group "Ops"' do
- group = create(:group, name: 'Ops')
- share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
- share_link.group_id = group.id
- share_link.save!
- end
-
- step 'project "Shop" is not shared with group "Market"' do
- create(:group, name: 'Market', path: 'market')
- end
-
- step 'I visit project group links page' do
- visit project_group_links_path(project)
- end
-
- def project
- @project ||= Project.find_by_name "Shop"
- end
-end
diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb
deleted file mode 100644
index b2d08515e77..00000000000
--- a/features/steps/project/project_milestone.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include WaitForRequests
-
- step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do
- project = Project.find_by(name: "Shop")
- milestone = project.milestones.find_by(title: 'v2.2')
- issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'feature')
- end
-
- step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do
- project = Project.find_by(name: "Shop")
- milestone = project.milestones.find_by(title: 'v2.2')
- issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'enhancement')
- end
-
- step 'project "Shop" has milestone "v2.2"' do
- project = Project.find_by(name: "Shop")
- milestone = create(:milestone,
- title: "v2.2",
- project: project,
- description: "# Description header"
- )
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- step 'I should see the list of labels' do
- expect(page).to have_selector('ul.manage-labels-list')
- end
-
- step 'I should see the labels "bug", "enhancement" and "feature"' do
- wait_for_requests
-
- page.within('#tab-issues') do
- expect(page).to have_content 'bug'
- expect(page).to have_content 'enhancement'
- expect(page).to have_content 'feature'
- end
- end
-
- step 'I should see the "bug" label listed only once' do
- page.within('#tab-labels') do
- expect(page).to have_content('bug', count: 1)
- end
- end
-
- step 'I click link "v2.2"' do
- click_link "v2.2"
- end
-
- step 'I click link "Labels"' do
- page.within('.nav-sidebar') do
- page.find(:xpath, "//a[@href='#tab-labels']").click
- end
- end
-end
diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb
deleted file mode 100644
index cebf09750b0..00000000000
--- a/features/steps/project/project_shortcuts.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedProjectTab
- include SharedShortcuts
-
- step 'I press "g" and "f"' do
- find('body').native.send_key('g')
- find('body').native.send_key('f')
- end
-
- step 'I press "g" and "c"' do
- find('body').native.send_key('g')
- find('body').native.send_key('c')
- end
-
- step 'I press "g" and "n"' do
- find('body').native.send_key('g')
- find('body').native.send_key('n')
- end
-
- step 'I press "g" and "d"' do
- find('body').native.send_key('g')
- find('body').native.send_key('d')
- end
-
- step 'I press "g" and "s"' do
- find('body').native.send_key('g')
- find('body').native.send_key('s')
- end
-
- step 'I press "g" and "w"' do
- find('body').native.send_key('g')
- find('body').native.send_key('w')
- end
-
- step 'I press "g" and "e"' do
- find('body').native.send_key('g')
- find('body').native.send_key('e')
- end
-end
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
deleted file mode 100644
index 7e2a357f6b2..00000000000
--- a/features/steps/project/services.rb
+++ /dev/null
@@ -1,224 +0,0 @@
-class Spinach::Features::ProjectServices < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I visit project "Shop" services page' do
- visit project_settings_integrations_path(@project)
- end
-
- step 'I should see list of available services' do
- expect(page).to have_content 'Project services'
- expect(page).to have_content 'Campfire'
- expect(page).to have_content 'HipChat'
- expect(page).to have_content 'Assembla'
- expect(page).to have_content 'Pushover'
- expect(page).to have_content 'Atlassian Bamboo'
- expect(page).to have_content 'JetBrains TeamCity'
- expect(page).to have_content 'Asana'
- expect(page).to have_content 'Irker (IRC gateway)'
- end
-
- step 'I should see service settings saved' do
- expect(find_field('Active').value).to eq '1'
- end
-
- step 'I click hipchat service link' do
- click_link 'HipChat'
- end
-
- step 'I fill hipchat settings' do
- check 'Active'
- fill_in 'Room', with: 'gitlab'
- fill_in 'Token', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the Hipchat success message' do
- expect(page).to have_content 'HipChat activated.'
- end
-
- step 'I fill hipchat settings with custom server' do
- check 'Active'
- fill_in 'Room', with: 'gitlab_custom'
- fill_in 'Token', with: 'secretCustom'
- fill_in 'Server', with: 'https://chat.example.com'
- click_button 'Save'
- end
-
- step 'I click pivotaltracker service link' do
- click_link 'PivotalTracker'
- end
-
- step 'I fill pivotaltracker settings' do
- check 'Active'
- fill_in 'Token', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the Pivotaltracker success message' do
- expect(page).to have_content 'PivotalTracker activated.'
- end
-
- step 'I click Flowdock service link' do
- click_link 'Flowdock'
- end
-
- step 'I fill Flowdock settings' do
- check 'Active'
- fill_in 'Token', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the Flowdock success message' do
- expect(page).to have_content 'Flowdock activated.'
- end
-
- step 'I click Assembla service link' do
- click_link 'Assembla'
- end
-
- step 'I fill Assembla settings' do
- check 'Active'
- fill_in 'Token', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the Assembla success message' do
- expect(page).to have_content 'Assembla activated.'
- end
-
- step 'I click Asana service link' do
- click_link 'Asana'
- end
-
- step 'I fill Asana settings' do
- check 'Active'
- fill_in 'Api key', with: 'verySecret'
- fill_in 'Restrict to branch', with: 'master'
- click_button 'Save'
- end
-
- step 'I should see the Asana success message' do
- expect(page).to have_content 'Asana activated.'
- end
-
- step 'I click email on push service link' do
- click_link 'Emails on push'
- end
-
- step 'I fill email on push settings' do
- check 'Active'
- fill_in 'Recipients', with: 'qa@company.name'
- click_button 'Save'
- end
-
- step 'I should see the Emails on push success message' do
- expect(page).to have_content 'Emails on push activated.'
- end
-
- step 'I click Irker service link' do
- click_link 'Irker (IRC gateway)'
- end
-
- step 'I fill Irker settings' do
- check 'Active'
- fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits'
- check 'Colorize messages'
- click_button 'Save'
- end
-
- step 'I should see the Irker success message' do
- expect(page).to have_content 'Irker (IRC gateway) activated.'
- end
-
- step 'I click Slack notifications service link' do
- click_link 'Slack notifications'
- end
-
- step 'I fill Slack notifications settings' do
- check 'Active'
- fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
- click_button 'Save'
- end
-
- step 'I should see the Slack notifications success message' do
- expect(page).to have_content 'Slack notifications activated.'
- end
-
- step 'I click Pushover service link' do
- click_link 'Pushover'
- end
-
- step 'I fill Pushover settings' do
- check 'Active'
- fill_in 'Api key', with: 'verySecret'
- fill_in 'User key', with: 'verySecret'
- fill_in 'Device', with: 'myDevice'
- select 'High Priority', from: 'Priority'
- select 'Bike', from: 'Sound'
- click_button 'Save'
- end
-
- step 'I should see the Pushover success message' do
- expect(page).to have_content 'Pushover activated.'
- end
-
- step 'I click jira service link' do
- click_link 'JIRA'
- end
-
- step 'I fill jira settings' do
- check 'Active'
-
- fill_in 'Web URL', with: 'http://jira.example'
- fill_in 'JIRA API URL', with: 'http://jira.example/api'
- fill_in 'Username', with: 'gitlab'
- fill_in 'Password', with: 'gitlab'
- click_button 'Save'
- end
-
- step 'I should see the JIRA success message' do
- expect(page).to have_content 'JIRA activated.'
- end
-
- step 'I click Atlassian Bamboo CI service link' do
- click_link 'Atlassian Bamboo CI'
- end
-
- step 'I fill Atlassian Bamboo CI settings' do
- check 'Active'
- fill_in 'Bamboo url', with: 'http://bamboo.example.com'
- fill_in 'Build key', with: 'KEY'
- fill_in 'Username', with: 'user'
- fill_in 'Password', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the Bamboo success message' do
- expect(page).to have_content 'Atlassian Bamboo CI activated.'
- end
-
- step 'I should see empty field Change Password' do
- click_link 'Atlassian Bamboo CI'
-
- expect(find_field('Enter new password').value).to be_nil
- end
-
- step 'I click JetBrains TeamCity CI service link' do
- click_link 'JetBrains TeamCity CI'
- end
-
- step 'I fill JetBrains TeamCity CI settings' do
- check 'Active'
- fill_in 'Teamcity url', with: 'http://teamcity.example.com'
- fill_in 'Build type', with: 'GitlabTest_Build'
- fill_in 'Username', with: 'user'
- fill_in 'Password', with: 'verySecret'
- click_button 'Save'
- end
-
- step 'I should see the JetBrains success message' do
- expect(page).to have_content 'JetBrains TeamCity CI activated.'
- end
-end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
deleted file mode 100644
index 2a1e6b2bce8..00000000000
--- a/features/steps/project/snippets.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
- include WaitForRequests
-
- step 'project "Shop" have "Snippet one" snippet' do
- create(:project_snippet,
- title: "Snippet one",
- content: "Test content",
- file_name: "snippet.rb",
- project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have no "Snippet two" snippet' do
- create(:snippet,
- title: "Snippet two",
- content: "Test content",
- file_name: "snippet.rb",
- author: project.users.first)
- end
-
- step 'I click link "New snippet"' do
- page.within '.nav-controls' do
- first(:link, "New snippet").click
- end
- end
-
- step 'I click link "Snippet one"' do
- click_link "Snippet one"
- end
-
- step 'I should see "Snippet one" in snippets' do
- expect(page).to have_content "Snippet one"
- end
-
- step 'I should not see "Snippet two" in snippets' do
- expect(page).not_to have_content "Snippet two"
- end
-
- step 'I should not see "Snippet one" in snippets' do
- expect(page).not_to have_content "Snippet one"
- end
-
- step 'I click link "Edit"' do
- page.within ".detail-page-header" do
- first(:link, "Edit").click
- end
- end
-
- step 'I click link "Delete"' do
- first(:link, "Delete").click
- end
-
- step 'I submit new snippet "Snippet three"' do
- fill_in "project_snippet_title", with: "Snippet three"
- fill_in "project_snippet_file_name", with: "my_snippet.rb"
- page.within('.file-editor') do
- find('.ace_editor').native.send_keys 'Content of snippet three'
- end
- click_button "Create snippet"
- wait_for_requests
- end
-
- step 'I should see snippet "Snippet three"' do
- expect(page).to have_content "Snippet three"
- expect(page).to have_content "Content of snippet three"
- end
-
- step 'I submit new title "Snippet new title"' do
- fill_in "project_snippet_title", with: "Snippet new title"
- click_button "Save"
- end
-
- step 'I should see "Snippet new title"' do
- expect(page).to have_content "Snippet new title"
- end
-
- step 'I leave a comment like "Good snippet!"' do
- page.within('.js-main-target-form') do
- fill_in "note_note", with: "Good snippet!"
- click_button "Comment"
- end
- wait_for_requests
- end
-
- step 'I should see comment "Good snippet!"' do
- expect(page).to have_content "Good snippet!"
- end
-
- step 'I visit snippet page "Snippet one"' do
- visit project_snippet_path(project, project_snippet)
- end
-
- def project_snippet
- @project_snippet ||= ProjectSnippet.find_by!(title: "Snippet one")
- end
-end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
deleted file mode 100644
index 5c4025ace34..00000000000
--- a/features/steps/project/team_management.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include Select2Helper
-
- 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 'I should see "Mike" in team list as "Reporter"' do
- user = User.find_by(name: 'Mike')
- project_member = project.project_members.find_by(user_id: user.id)
- page.within "#project_member_#{project_member.id}" do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'gitlab user "Mike"' do
- create(:user, name: "Mike")
- end
-
- step 'gitlab user "Dmitriy"' do
- create(:user, name: "Dmitriy")
- end
-
- step '"Dmitriy" is "Shop" developer' do
- user = User.find_by(name: "Dmitriy")
- project = Project.find_by(name: "Shop")
- project.team << [user, :developer]
- end
-
- step 'I own project "Website"' do
- @project = create(:project, name: "Website", namespace: @user.namespace)
- @project.team << [@user, :master]
- end
-
- step '"Mike" is "Website" reporter' do
- user = User.find_by(name: "Mike")
- project = Project.find_by(name: "Website")
- project.team << [user, :reporter]
- end
-
- step 'I click link "Import team from another project"' do
- page.within '.users-project-form' do
- click_link "Import"
- end
- end
-
- When 'I submit "Website" project for import team' do
- project = Project.find_by(name: "Website")
- select project.name_with_namespace, from: 'source_project_id'
- click_button 'Import'
- end
-
- step 'I click cancel link for "Dmitriy"' 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_link('Remove user from project')
- end
- end
-
- step 'I share project with group "OpenSource"' do
- project = Project.find_by(name: 'Shop')
- os_group = create(:group, name: 'OpenSource')
- create(:project, group: os_group)
- @os_user1 = create(:user)
- @os_user2 = create(:user)
- os_group.add_owner(@os_user1)
- os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
- share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
- share_link.group_id = os_group.id
- share_link.save!
- end
-
- step 'I should see "Opensource" group user listing' do
- page.within '.project-members-groups' do
- expect(page).to have_content('OpenSource')
- expect(first('.group_member')).to have_content('Master')
- end
- end
-end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
deleted file mode 100644
index 855757e34b3..00000000000
--- a/features/steps/project/wiki.rb
+++ /dev/null
@@ -1,195 +0,0 @@
-class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
-
- step 'I click on the Cancel button' do
- page.within(:css, ".wiki-form .form-actions") do
- click_on "Cancel"
- end
- end
-
- step 'I should be redirected back to the Edit Home Wiki page' do
- expect(current_path).to eq project_wiki_path(project, :home)
- end
-
- step 'I create the Wiki Home page' do
- fill_in "wiki_content", with: '[link test](test)'
- page.within '.wiki-form' do
- click_on "Create page"
- end
- end
-
- step 'I create the Wiki Home page with no content' do
- fill_in "wiki_content", with: ''
- page.within '.wiki-form' do
- click_on "Create page"
- end
- end
-
- step 'I should see the newly created wiki page' do
- expect(page).to have_content "Home"
- expect(page).to have_content "link test"
-
- click_link "link test"
- expect(page).to have_content "Create page"
- end
-
- step 'I have an existing Wiki page' do
- wiki.create_page("existing", "content", :markdown, "first commit")
- @page = wiki.find_page("existing")
- end
-
- step 'I browse to that Wiki page' do
- visit project_wiki_path(project, @page)
- end
-
- step 'I click on the Edit button' do
- click_on "Edit"
- end
-
- step 'I change the content' do
- fill_in "Content", with: 'Updated Wiki Content'
- click_on "Save changes"
- end
-
- step 'I should see the updated content' do
- expect(page).to have_content "Updated Wiki Content"
- end
-
- step 'I should be redirected back to that Wiki page' do
- expect(current_path).to eq project_wiki_path(project, @page)
- end
-
- step 'That page has two revisions' do
- @page.update(content: "new content", message: "second commit")
- end
-
- step 'I click the History button' do
- click_on 'Page history'
- end
-
- step 'I should see both revisions' do
- expect(page).to have_content current_user.name
- expect(page).to have_content "first commit"
- expect(page).to have_content "second commit"
- end
-
- step 'I click on the "Delete this page" button' do
- click_on "Delete"
- end
-
- step 'The page should be deleted' do
- expect(page).to have_content "Page was successfully deleted"
- end
-
- step 'I should see the existing page in the pages list' do
- expect(page).to have_content current_user.name
- expect(find('.wiki-pages')).to have_content @page.title.capitalize
- end
-
- step 'I have an existing Wiki page with images linked on page' do
- wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit")
- @wiki_page = wiki.find_page("pictures")
- end
-
- step 'I browse to wiki page with images' do
- visit project_wiki_path(project, @wiki_page)
- end
-
- step 'I click on existing image link' do
- file = Gollum::File.new(wiki.wiki)
- Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file)
- Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg")
- expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
- click_on "image"
- end
-
- step 'I should see the image from wiki repo' do
- expect(current_path).to match('wikis/image.jpg')
- expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
- Gollum::Wiki.any_instance.unstub(:file)
- Gollum::File.any_instance.unstub(:mime_type)
- end
-
- step 'Image should be shown on the page' do
- expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]")
- end
-
- step 'I click on image link' do
- expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
- click_on "image"
- end
-
- step 'I should see the new wiki page form' do
- expect(current_path).to match('wikis/image.jpg')
- expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Create page')
- end
-
- step 'I create a New page with paths' do
- click_on 'New page'
- fill_in 'Page slug', with: 'one/two/three-test'
- page.within '#modal-new-wiki' do
- click_on 'Create page'
- end
- fill_in "wiki_content", with: 'wiki content'
- page.within '.wiki-form' do
- click_on "Create page"
- end
- expect(current_path).to include 'one/two/three-test'
- end
-
- step 'I should see non-escaped link in the pages list' do
- expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
- end
-
- step 'I edit the Wiki page with a path' do
- expect(find('.wiki-pages')).to have_content('Three')
- click_on 'Three'
- expect(find('.nav-text')).to have_content('Three')
- click_on 'Edit'
- end
-
- step 'I should see a non-escaped path' do
- expect(current_path).to include 'one/two/three-test'
- end
-
- step 'I should see the Editing page' do
- expect(page).to have_content('Edit Page')
- end
-
- step 'I view the page history of a Wiki page that has a path' do
- click_on 'Three'
- click_on 'Page history'
- end
-
- step 'I click on Page History' do
- click_on 'Page history'
- end
-
- step 'I should see the page history' do
- page.within(:css, ".nav-text") do
- expect(page).to have_content('History')
- end
- end
-
- step 'I search for Wiki content' do
- fill_in "Search", with: "wiki_content"
- click_button "Search"
- end
-
- step 'I should see a link with a version ID' do
- find('a[href*="?version_id"]')
- end
-
- step 'I should see a "Content can\'t be blank" error message' do
- expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content('Content can\'t be blank')
- end
-
- def wiki
- @project_wiki = ProjectWiki.new(project, current_user)
- end
-end
diff --git a/features/steps/search.rb b/features/steps/search.rb
deleted file mode 100644
index 16c4a5ab2e4..00000000000
--- a/features/steps/search.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-class Spinach::Features::Search < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I search for "Sho"' do
- fill_in "dashboard_search", with: "Sho"
- click_button "Search"
- end
-
- step 'I search for "Foo"' do
- fill_in "dashboard_search", with: "Foo"
- find('.btn-search').trigger('click')
- end
-
- step 'I search for "rspec"' do
- fill_in "dashboard_search", with: "rspec"
- find('.btn-search').trigger('click')
- end
-
- step 'I search for "rspec" on project page' do
- fill_in "search", with: "rspec"
- click_button "Go"
- end
-
- step 'I search for "Wiki content"' do
- fill_in "dashboard_search", with: "content"
- find('.btn-search').trigger('click')
- end
-
- step 'I click "Issues" link' do
- page.within '.search-filter' do
- click_link 'Issues'
- end
- end
-
- step 'I click project "Shop" link' do
- find('.js-search-project-dropdown').trigger('click')
- page.within '.project-filter' do
- click_link project.name_with_namespace
- end
- end
-
- step 'I click "Merge requests" link' do
- page.within '.search-filter' do
- click_link 'Merge requests'
- end
- end
-
- step 'I click "Milestones" link' do
- page.within '.search-filter' do
- click_link 'Milestones'
- end
- end
-
- step 'I click "Wiki" link' do
- page.within '.search-filter' do
- click_link 'Wiki'
- end
- end
-
- step 'I should see "Shop" project link' do
- expect(page).to have_link "Shop"
- end
-
- step 'I should see code results for project "Shop"' do
- page.within('.results') do
- page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions'
- end
- end
-
- step 'I search for "Contibuting"' do
- fill_in "dashboard_search", with: "Contibuting"
- click_button "Search"
- end
-
- step 'project has issues' do
- create(:issue, title: "Foo", project: project)
- create(:issue, title: "Bar", project: project)
- end
-
- step 'project has merge requests' do
- create(:merge_request, title: "Foo", source_project: project, target_project: project)
- create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project)
- end
-
- step 'project has milestones' do
- create(:milestone, title: "Foo", project: project)
- create(:milestone, title: "Bar", project: project)
- end
-
- step 'I should see "Foo" link in the search results' do
- page.within('.results') do
- find(:css, '.search-results').should have_link 'Foo'
- end
- end
-
- step 'I should not see "Bar" link in the search results' do
- expect(find(:css, '.search-results')).not_to have_link 'Bar'
- end
-
- step 'I should see "test_wiki" link in the search results' do
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link 'test_wiki'
- end
- end
-
- step 'project has Wiki content' do
- @wiki = ::ProjectWiki.new(project, current_user)
- @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
- end
-
- step 'project "Shop" is public' do
- project.update_attributes(visibility_level: Project::PUBLIC)
- end
-end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 2c59ec5bb06..c872bd6f861 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -232,7 +232,7 @@ module SharedDiffNote
end
def click_parallel_diff_line(code, line_type)
- find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover'
+ find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover'
find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 7c842ba88fb..714985f2051 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -109,10 +109,10 @@ module SharedIssuable
edit_issuable
end
- step 'I sort the list by "Oldest updated"' do
+ step 'I sort the list by "Last updated"' do
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
- click_link "Oldest updated"
+ click_link "Last updated"
end
end
@@ -124,16 +124,16 @@ module SharedIssuable
end
end
- step 'I sort the list by "Most popular"' do
+ step 'I sort the list by "Popularity"' do
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
- click_link 'Most popular'
+ click_link 'Popularity'
end
end
- step 'The list should be sorted by "Oldest updated"' do
- expect(find('.issues-filters')).to have_content('Oldest updated')
+ step 'The list should be sorted by "Last updated"' do
+ expect(find('.issues-filters')).to have_content('Last updated')
end
step 'I click link "Next" in the sidebar' do
diff --git a/features/support/env.rb b/features/support/env.rb
index 608d988755c..5962745d501 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -10,7 +10,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
-%w(select2_helper test_env repo_helpers wait_for_requests sidekiq).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ee4e1688e12..79e55a2f4f7 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -8,7 +8,6 @@ module API
logger: Logger.new(LOG_FILENAME),
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
- GrapeLogging::Loggers::Response.new,
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new
]
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index c4c0fdda665..e79f988f549 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -75,7 +75,7 @@ module API
raise RevokedError
when AccessTokenValidationService::VALID
- @current_user = User.find(access_token.resource_owner_id)
+ User.find(access_token.resource_owner_id)
end
end
@@ -84,11 +84,13 @@ module API
return nil unless token_string.present?
- find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
- end
+ user =
+ find_user_by_authentication_token(token_string) ||
+ find_user_by_personal_access_token(token_string, scopes)
+
+ raise UnauthorizedError unless user
- def current_user
- @current_user
+ user
end
private
@@ -107,7 +109,16 @@ module API
end
def find_access_token
- @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
+ return @access_token if defined?(@access_token)
+
+ token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
+ return @access_token = nil unless token
+
+ @access_token = Doorkeeper::AccessToken.by_token(token)
+ raise UnauthorizedError unless @access_token
+
+ @access_token.revoke_previous_refresh_token!
+ @access_token
end
def doorkeeper_request
@@ -169,6 +180,7 @@ module API
TokenNotFoundError = Class.new(StandardError)
ExpiredError = Class.new(StandardError)
RevokedError = Class.new(StandardError)
+ UnauthorizedError = Class.new(StandardError)
class InsufficientScopeError < StandardError
attr_reader :scopes
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 642c1140fcc..61a2d688282 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository branches' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
use :pagination
@@ -21,12 +21,15 @@ module API
get ':id/repository/branches' do
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
- present paginate(branches), with: Entities::RepoBranch, project: user_project
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ present paginate(branches), with: Entities::Branch, project: user_project
+ end
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
desc 'Get a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -38,7 +41,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
end
@@ -47,7 +50,7 @@ module API
# in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
desc 'Protect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -77,7 +80,7 @@ module API
end
if protected_branch.valid?
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
@@ -85,7 +88,7 @@ module API
# Note: This API will be deprecated in favor of the protected branches API.
desc 'Unprotect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -98,11 +101,11 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
desc 'Create branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -116,7 +119,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: Entities::RepoBranch,
+ with: Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4b8d248f5f7..4af37a2ad1d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -46,11 +46,11 @@ module API
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present paginate(paginated_commits), with: Entities::RepoCommit
+ present paginate(paginated_commits), with: Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -72,14 +72,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result])
- present commit_detail, with: Entities::RepoCommitDetail
+ present commit_detail, with: Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
failure [[404, 'Commit Not Found']]
end
params do
@@ -90,7 +90,7 @@ module API
not_found! 'Commit' unless commit
- present commit, with: Entities::RepoCommitDetail
+ present commit, with: Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -104,7 +104,7 @@ module API
not_found! 'Commit' unless commit
- present commit.raw_diffs.to_a, with: Entities::RepoDiff
+ present commit.raw_diffs.to_a, with: Entities::Diff
end
desc "Get a commit's comments" do
@@ -126,7 +126,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
@@ -151,7 +151,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/custom_attributes_endpoints.rb b/lib/api/custom_attributes_endpoints.rb
new file mode 100644
index 00000000000..5000aa0d9ac
--- /dev/null
+++ b/lib/api/custom_attributes_endpoints.rb
@@ -0,0 +1,77 @@
+module API
+ module CustomAttributesEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ attributable_class = name.demodulize.singularize
+ attributable_key = attributable_class.underscore
+ attributable_name = attributable_class.humanize(capitalize: false)
+ attributable_finder = "find_#{attributable_key}"
+
+ helpers do
+ params :custom_attributes_key do
+ requires :key, type: String, desc: 'The key of the custom attribute'
+ end
+ end
+
+ desc "Get all custom attributes on a #{attributable_name}" do
+ success Entities::CustomAttribute
+ end
+ get ':id/custom_attributes' do
+ resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
+ authorize! :read_custom_attribute
+
+ present resource.custom_attributes, with: Entities::CustomAttribute
+ end
+
+ desc "Get a custom attribute on a #{attributable_name}" do
+ success Entities::CustomAttribute
+ end
+ params do
+ use :custom_attributes_key
+ end
+ get ':id/custom_attributes/:key' do
+ resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
+ authorize! :read_custom_attribute
+
+ custom_attribute = resource.custom_attributes.find_by!(key: params[:key])
+
+ present custom_attribute, with: Entities::CustomAttribute
+ end
+
+ desc "Set a custom attribute on a #{attributable_name}"
+ params do
+ use :custom_attributes_key
+ requires :value, type: String, desc: 'The value of the custom attribute'
+ end
+ put ':id/custom_attributes/:key' do
+ resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
+ authorize! :update_custom_attribute
+
+ custom_attribute = resource.custom_attributes
+ .find_or_initialize_by(key: params[:key])
+
+ custom_attribute.update(value: params[:value])
+
+ if custom_attribute.valid?
+ present custom_attribute, with: Entities::CustomAttribute
+ else
+ render_validation_error!(custom_attribute)
+ end
+ end
+
+ desc "Delete a custom attribute on a #{attributable_name}"
+ params do
+ use :custom_attributes_key
+ end
+ delete ':id/custom_attributes/:key' do
+ resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
+ authorize! :update_custom_attribute
+
+ resource.custom_attributes.find_by!(key: params[:key]).destroy
+
+ status 204
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 52c49e5caa9..5f0bad14839 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -89,6 +89,9 @@ module API
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :name, :name_with_namespace
expose :path, :path_with_namespace
+ expose :avatar_url do |project, options|
+ project.avatar_url(only_path: false)
+ end
expose :star_count, :forks_count
expose :created_at, :last_activity_at
end
@@ -146,9 +149,7 @@ module API
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? }
expose :import_status
expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :avatar_url do |user, options|
- user.avatar_url(only_path: false)
- end
+
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs
@@ -193,8 +194,8 @@ module API
class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled
- expose :avatar_url do |user, options|
- user.avatar_url(only_path: false)
+ expose :avatar_url do |group, options|
+ group.avatar_url(only_path: false)
end
expose :web_url
expose :request_access_enabled
@@ -219,7 +220,7 @@ module API
expose :shared_projects, using: Entities::Project
end
- class RepoCommit < Grape::Entity
+ class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
expose :safe_message, as: :message
@@ -227,24 +228,28 @@ module API
expose :committer_name, :committer_email, :committed_date
end
- class RepoCommitStats < Grape::Entity
+ class CommitStats < Grape::Entity
expose :additions, :deletions, :total
end
- class RepoCommitDetail < RepoCommit
- expose :stats, using: Entities::RepoCommitStats
+ class CommitDetail < Commit
+ expose :stats, using: Entities::CommitStats
expose :status
+ expose :last_pipeline, using: 'API::Entities::PipelineBasic'
end
- class RepoBranch < Grape::Entity
+ class Branch < Grape::Entity
expose :name
- expose :commit, using: Entities::RepoCommit do |repo_branch, options|
+ expose :commit, using: Entities::Commit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
expose :merged do |repo_branch, options|
- options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ end
end
expose :protected do |repo_branch, options|
@@ -260,7 +265,7 @@ module API
end
end
- class RepoTreeObject < Grape::Entity
+ class TreeObject < Grape::Entity
expose :id, :name, :type, :path
expose :mode do |obj, options|
@@ -300,7 +305,7 @@ module API
expose :state, :created_at, :updated_at
end
- class RepoDiff < Grape::Entity
+ class Diff < Grape::Entity
expose :old_path, :new_path, :a_mode, :b_mode
expose :new_file?, as: :new_file
expose :renamed_file?, as: :renamed_file
@@ -332,6 +337,7 @@ module API
end
class IssueBasic < ProjectEntity
+ expose :closed_at
expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort
@@ -362,6 +368,7 @@ module API
end
expose :due_date
expose :confidential
+ expose :discussion_locked
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
@@ -458,6 +465,7 @@ module API
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :user_notes_count
+ expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -477,7 +485,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -488,9 +496,9 @@ module API
end
class MergeRequestDiffFull < MergeRequestDiff
- expose :commits, using: Entities::RepoCommit
+ expose :commits, using: Entities::Commit
- expose :diffs, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -586,8 +594,7 @@ module API
expose :target_type
expose :target do |todo, options|
- target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
- Entities.const_get(target).represent(todo.target, options)
+ Entities.const_get(todo.target_type).represent(todo.target, options)
end
expose :target_url do |todo, options|
@@ -723,15 +730,15 @@ module API
end
class Compare < Grape::Entity
- expose :commit, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil).last
+ expose :commit, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil).last
end
- expose :commits, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil)
+ expose :commits, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil)
end
- expose :diffs, using: Entities::RepoDiff do |compare, options|
+ expose :diffs, using: Entities::Diff do |compare, options|
compare.diffs(limits: false).to_a
end
@@ -767,10 +774,10 @@ module API
expose :description
end
- class RepoTag < Grape::Entity
+ class Tag < Grape::Entity
expose :name, :message
- expose :commit, using: Entities::RepoCommit do |repo_tag, options|
+ expose :commit, using: Entities::Commit do |repo_tag, options|
options[:project].repository.commit(repo_tag.dereferenced_target)
end
@@ -821,7 +828,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
- expose :commit, with: RepoCommit
+ expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
@@ -874,7 +881,7 @@ module API
expose :deployable, using: Entities::Job
end
- class RepoLicense < Grape::Entity
+ class License < Grape::Entity
expose :key, :name, :nickname
expose :featured, as: :popular
expose :url, as: :html_url
@@ -1016,6 +1023,7 @@ module API
expose :cache, using: Cache
expose :credentials, using: Credentials
expose :dependencies, using: Dependency
+ expose :features
end
end
@@ -1030,5 +1038,10 @@ module API
expose :failing_on_hosts
expose :total_failures
end
+
+ class CustomAttribute < Grape::Entity
+ expose :key
+ expose :value
+ end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 6a0be74623e..67ef329f01f 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,6 +3,8 @@ module API
include Gitlab::Utils
include Helpers::Pagination
+ UnauthorizedError = Class.new(StandardError)
+
SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
@@ -139,7 +141,7 @@ module API
end
def authenticate!
- unauthorized! unless current_user && can?(initial_current_user, :access_api)
+ unauthorized! unless current_user
end
def authenticate_non_get!
@@ -285,7 +287,7 @@ module API
if sentry_enabled? && report_exception?(exception)
define_params_for_grape_middleware
sentry_context
- Raven.capture_exception(exception)
+ Raven.capture_exception(exception, extra: params)
end
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
@@ -397,19 +399,27 @@ module API
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
- Gitlab::Auth::UniqueIpsLimiter.limit_user! do
- @initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint)
- @initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint)
- @initial_current_user ||= find_user_from_warden
-
- unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
- @initial_current_user = nil
- end
- @initial_current_user
+ begin
+ @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
+ rescue APIGuard::UnauthorizedError, UnauthorizedError
+ unauthorized!
end
end
+ def find_current_user
+ user =
+ find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
+ doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
+ find_user_from_warden
+
+ return nil unless user
+
+ raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+
+ user
+ end
+
def sudo!
return unless sudo_identifier
return unless initial_current_user
@@ -454,10 +464,12 @@ module API
header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
end
- # The Grape Error Middleware only has access to env but no params. We workaround this by
- # defining a method that returns the right value.
+ # The Grape Error Middleware only has access to `env` but not `params` nor
+ # `request`. We workaround this by defining methods that returns the right
+ # values.
def define_params_for_grape_middleware
- self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys }
+ self.define_singleton_method(:request) { Rack::Request.new(env) }
+ self.define_singleton_method(:params) { request.params.symbolize_keys }
end
# We could get a Grape or a standard Ruby exception. We should only report anything that
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index c0fef56378f..6e78ac2c903 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -31,6 +31,12 @@ module API
protocol = params[:protocol]
actor.update_last_used_at if actor.is_a?(Key)
+ user =
+ if actor.is_a?(Key)
+ actor.user
+ else
+ actor
+ end
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass
@@ -47,6 +53,7 @@ module API
{
status: true,
gl_repository: gl_repository,
+ gl_username: user&.username,
repository_path: repository_path,
gitaly: gitaly_payload(params[:action])
}
@@ -136,7 +143,7 @@ module API
codes = nil
- ::Users::UpdateService.new(user).execute! do |user|
+ ::Users::UpdateService.new(current_user, user: user).execute! do |user|
codes = user.generate_otp_backup_codes!
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1729df2aad0..0df41dcc903 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -48,6 +48,7 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
end
params :issue_params do
@@ -193,7 +194,7 @@ module API
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
- at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
+ at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, :discussion_locked,
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_iid' do
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index ae43a4a3237..d202eaa4c49 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -6,7 +6,7 @@ module API
requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
end
post '/lint' do
- error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
+ error = Gitlab::Ci::YamlProcessor.validation_message(params[:content])
status 200
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 56d72d511da..be843ec8251 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,7 +2,7 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
@@ -55,6 +55,7 @@ module API
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end
get do
+ authenticate! unless params[:scope] == 'all'
merge_requests = find_merge_requests
options = { with: Entities::MergeRequestBasic,
@@ -182,13 +183,13 @@ module API
end
desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
+ success Entities::Commit
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
- present paginate(commits), with: Entities::RepoCommit
+ present paginate(commits), with: Entities::Commit
end
desc 'Show the merge request changes' do
@@ -213,12 +214,14 @@ module API
:remove_source_branch,
:state_event,
:target_branch,
- :title
+ :title,
+ :discussion_locked
]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
+ optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
use :optional_params
at_least_one_of(*at_least_one_of_ce)
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d6e7203adaf..0b9ab4eeb05 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -78,6 +78,8 @@ module API
}
if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ authorize! :create_note, noteable
+
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index bcc0833aa5c..0266bf2f717 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -35,7 +35,7 @@ module API
new_notification_email = params.delete(:notification_email)
if new_notification_email
- ::Users::UpdateService.new(current_user, notification_email: new_notification_email).execute
+ ::Users::UpdateService.new(current_user, user: current_user, notification_email: new_notification_email).execute
end
notification_setting.update(declared_params(include_missing: false))
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 7dc19788462..aab7a6c3f93 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -70,8 +70,11 @@ module API
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
- def present_projects(options = {})
- projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ def load_projects
+ ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ end
+
+ def present_projects(projects, options = {})
projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
@@ -111,7 +114,7 @@ module API
params[:user] = user
- present_projects
+ present_projects load_projects
end
end
@@ -124,7 +127,7 @@ module API
use :statistics_params
end
get do
- present_projects
+ present_projects load_projects
end
desc 'Create new project' do
@@ -229,6 +232,18 @@ module API
end
end
+ desc 'List forks of this project' do
+ success Entities::Project
+ end
+ params do
+ use :collection_params
+ end
+ get ':id/forks' do
+ forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
+
+ present_projects forks
+ end
+
desc 'Update an existing project' do
success Entities::Project
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2255fb1b70d..ceee3226732 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -35,7 +35,7 @@ module API
end
desc 'Get a project repository tree' do
- success Entities::RepoTreeObject
+ success Entities::TreeObject
end
params do
optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -52,7 +52,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
entries = ::Kaminari.paginate_array(tree.sorted_entries)
- present paginate(entries), with: Entities::RepoTreeObject
+ present paginate(entries), with: Entities::TreeObject
end
desc 'Get raw blob contents from the repository'
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 912415e3a7f..0d394a7b441 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -11,18 +11,18 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository tags' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
use :pagination
end
get ':id/repository/tags' do
tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
- present paginate(tags), with: Entities::RepoTag, project: user_project
+ present paginate(tags), with: Entities::Tag, project: user_project
end
desc 'Get a single repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -31,11 +31,11 @@ module API
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
- present tag, with: Entities::RepoTag, project: user_project
+ present tag, with: Entities::Tag, project: user_project
end
desc 'Create a new repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -51,7 +51,7 @@ module API
if result[:status] == :success
present result[:tag],
- with: Entities::RepoTag,
+ with: Entities::Tag,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index f70bc0622b7..6550b331fb8 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -49,7 +49,7 @@ module API
desc 'Get the list of the available license template' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -60,12 +60,12 @@ module API
featured: declared(params)[:popular].present? ? true : nil
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options))
- present paginate(licences), with: Entities::RepoLicense
+ present paginate(licences), with: Entities::License
end
desc 'Get the text for a specific license' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -75,7 +75,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1825c90a23b..b6f97a1eac2 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -6,12 +6,14 @@ module API
allow_access_with_scope :read_user, if: -> (request) { request.get? }
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ include CustomAttributesEndpoints
+
before do
authenticate_non_get!
end
helpers do
- def find_user(params)
+ def find_user_by_id(params)
id = params[:user_id] || params[:id]
User.find_by(id: id) || not_found!('User')
end
@@ -88,7 +90,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
- opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {}
+ opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
present user, opts
end
@@ -166,7 +168,7 @@ module API
user_params[:password_expires_at] = Time.now if user_params[:password].present?
- result = ::Users::UpdateService.new(user, user_params.except(:extern_uid, :provider)).execute
+ result = ::Users::UpdateService.new(current_user, user_params.except(:extern_uid, :provider).merge(user: user)).execute
if result[:status] == :success
present user, with: Entities::UserPublic
@@ -326,10 +328,9 @@ module API
user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
- email = Emails::CreateService.new(user, declared_params(include_missing: false)).execute
+ email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -367,10 +368,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: user).execute(email)
end
-
- user.update_secondary_emails!
end
desc 'Delete a user. Available only for admins.' do
@@ -430,7 +429,7 @@ module API
resource :impersonation_tokens do
helpers do
def finder(options = {})
- user = find_user(params)
+ user = find_user_by_id(params)
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
@@ -672,10 +671,9 @@ module API
requires :email, type: String, desc: 'The new email'
end
post "emails" do
- email = Emails::CreateService.new(current_user, declared_params).execute
+ email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -691,10 +689,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: current_user).execute(email)
end
-
- current_user.update_secondary_emails!
end
desc 'Get a list of user activities'
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 81b13249892..69cd12de72c 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -11,12 +11,12 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name)
- present branches, with: ::API::Entities::RepoBranch, project: user_project
+ present branches, with: ::API::Entities::Branch, project: user_project
end
desc 'Delete a branch'
@@ -47,7 +47,7 @@ module API
end
desc 'Create branch' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
params do
requires :branch_name, type: String, desc: 'The name of the branch'
@@ -60,7 +60,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: ::API::Entities::RepoBranch,
+ with: ::API::Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 5936f4700aa..345cb7e7c11 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -34,11 +34,11 @@ module API
after: params[:since],
before: params[:until])
- present commits, with: ::API::Entities::RepoCommit
+ present commits, with: ::API::Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -59,14 +59,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commits(result[:result], limit: 1).first
- present commit_detail, with: ::API::Entities::RepoCommitDetail
+ present commit_detail, with: ::API::Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
failure [[404, 'Not Found']]
end
params do
@@ -77,7 +77,7 @@ module API
not_found! "Commit" unless commit
- present commit, with: ::API::Entities::RepoCommitDetail
+ present commit, with: ::API::Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -113,7 +113,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
@@ -138,7 +138,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index c928ce5265b..afdd7b83998 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -220,7 +220,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: ::API::Entities::User
expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: ::API::Entities::RepoCommit
+ expose :commit, with: ::API::Entities::Commit
expose :runner, with: ::API::Entities::Runner
expose :pipeline, with: ::API::Entities::PipelineBasic
end
@@ -237,7 +237,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index b6b7254ae29..1d6d823f32b 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -135,12 +135,12 @@ module API
end
desc 'Get the commits of a merge request' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
get "#{path}/commits" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.commits, with: ::API::Entities::RepoCommit
+ present merge_request.commits, with: ::API::Entities::Commit
end
desc 'Show the merge request changes' do
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 0eaa0de2eef..41a7c6b83ae 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -19,7 +19,7 @@ module API
end
desc 'Get a project repository tree' do
- success ::API::Entities::RepoTreeObject
+ success ::API::Entities::TreeObject
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -35,7 +35,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
- present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
+ present tree.sorted_entries, with: ::API::Entities::TreeObject
end
desc 'Get a raw file contents'
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 7e5875cd030..6e37d31d153 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -8,11 +8,11 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
- success ::API::Entities::RepoTag
+ success ::API::Entities::Tag
end
get ":id/repository/tags" do
tags = user_project.repository.tags.sort_by(&:name).reverse
- present tags, with: ::API::Entities::RepoTag, project: user_project
+ present tags, with: ::API::Entities::Tag, project: user_project
end
desc 'Delete a repository tag'
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
index 2a2fb59045c..7298203df10 100644
--- a/lib/api/v3/templates.rb
+++ b/lib/api/v3/templates.rb
@@ -52,7 +52,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -61,7 +61,7 @@ module API
options = {
featured: declared(params)[:popular].present? ? true : nil
}
- present Licensee::License.all(options), with: ::API::Entities::RepoLicense
+ present Licensee::License.all(options), with: ::API::Entities::License
end
end
@@ -70,7 +70,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -80,7 +80,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index b9a573d3542..3cf3939994a 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -79,7 +79,7 @@ module Backup
# - 1495527122_gitlab_backup.tar
# - 1495527068_2017_05_23_gitlab_backup.tar
# - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar
- next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+.*)?)?_gitlab_backup\.tar$/
+ next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?(-ee)?)?)?_gitlab_backup\.tar$/
timestamp = $1.to_i
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 4e92be85110..3ad09a1b421 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -78,7 +78,7 @@ module Backup
project.ensure_storage_path_exists
cmd = if File.exist?(path_to_project_bundle)
- %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
+ %W(#{Gitlab.config.git.bin_path} clone --bare --mirror #{path_to_project_bundle} #{path_to_project_repo})
else
%W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
end
diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb
index bcb4f332267..4cd9b02b76c 100644
--- a/lib/banzai/filter/image_lazy_load_filter.rb
+++ b/lib/banzai/filter/image_lazy_load_filter.rb
@@ -1,6 +1,7 @@
module Banzai
module Filter
- # HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded
+ # HTML filter that moves the value of image `src` attributes to `data-src`
+ # so they can be lazy loaded.
class ImageLazyLoadFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img').each do |img|
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index ee73fa91589..9cac303e645 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -1,6 +1,18 @@
module Banzai
module Filter
class MarkdownFilter < HTML::Pipeline::TextFilter
+ # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
+ REDCARPET_OPTIONS = {
+ fenced_code_blocks: true,
+ footnotes: true,
+ lax_spacing: true,
+ no_intra_emphasis: true,
+ space_after_headers: true,
+ strikethrough: true,
+ superscript: true,
+ tables: true
+ }.freeze
+
def initialize(text, context = nil, result = nil)
super text, context, result
@text = @text.delete "\r"
@@ -13,27 +25,11 @@ module Banzai
end
def self.renderer
- @renderer ||= begin
+ Thread.current[:banzai_markdown_renderer] ||= begin
renderer = Banzai::Renderer::HTML.new
- Redcarpet::Markdown.new(renderer, redcarpet_options)
+ Redcarpet::Markdown.new(renderer, REDCARPET_OPTIONS)
end
end
-
- def self.redcarpet_options
- # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
- @redcarpet_options ||= {
- fenced_code_blocks: true,
- footnotes: true,
- lax_spacing: true,
- no_intra_emphasis: true,
- space_after_headers: true,
- strikethrough: true,
- superscript: true,
- tables: true
- }.freeze
- end
-
- private_class_method :redcarpet_options
end
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 2d6e8ffc90f..d8c8deea628 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -5,6 +5,7 @@ module Banzai
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
+ TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/
def whitelist
whitelist = super
@@ -24,7 +25,8 @@ module Banzai
# Only push these customizations once
return if customized?(whitelist[:transformers])
- # Allow table alignment
+ # Allow table alignment; we whitelist specific style properties in a
+ # transformer below
whitelist[:attributes]['th'] = %w(style)
whitelist[:attributes]['td'] = %w(style)
@@ -43,6 +45,10 @@ module Banzai
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
+ # Disallow `name` attribute globally, allow on `a`
+ whitelist[:attributes][:all].delete('name')
+ whitelist[:attributes]['a'].push('name')
+
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
@@ -52,6 +58,9 @@ module Banzai
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(self.class.remove_rel)
+ # Remove any `style` properties not required for table alignment
+ whitelist[:transformers].push(self.class.remove_unsafe_table_style)
+
whitelist
end
@@ -64,8 +73,9 @@ module Banzai
return unless node.has_attribute?('href')
begin
+ node['href'] = node['href'].strip
uri = Addressable::URI.parse(node['href'])
- uri.scheme = uri.scheme.strip.downcase if uri.scheme
+ uri.scheme = uri.scheme.downcase if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError
@@ -81,6 +91,21 @@ module Banzai
end
end
end
+
+ def remove_unsafe_table_style
+ lambda do |env|
+ node = env[:node]
+
+ return unless node.name == 'th' || node.name == 'td'
+ return unless node.has_attribute?('style')
+
+ if node['style'] =~ TABLE_ALIGNMENT_PATTERN
+ node['style'] = "text-align: #{$~[:alignment]}"
+ else
+ node.remove_attribute('style')
+ end
+ end
+ end
end
end
end
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
index e47c384afc1..8f5f144d582 100644
--- a/lib/banzai/pipeline/email_pipeline.rb
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -1,6 +1,12 @@
module Banzai
module Pipeline
class EmailPipeline < FullPipeline
+ def self.filters
+ super.tap do |filter_array|
+ filter_array.delete(Banzai::Filter::ImageLazyLoadFilter)
+ end
+ end
+
def self.transform_context(context)
super(context).merge(
only_path: false
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index a65bbe23958..e0a8ca653cb 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -34,7 +34,8 @@ module Banzai
{ namespace: :owner },
{ group: [:owners, :group_members] },
:invited_groups,
- :project_members
+ :project_members,
+ :project_feature
]
}
),
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index ceca9296851..5f91884a878 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -40,7 +40,7 @@ module Banzai
return cacheless_render_field(object, field)
end
- object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
+ object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
object.cached_html_for(field)
end
@@ -162,10 +162,5 @@ module Banzai
return unless cache_key
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
-
- # GitLab EE needs to disable updates on GET requests in Geo
- def self.update_object?(object)
- true
- end
end
end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
deleted file mode 100644
index b9e9f9f7f4a..00000000000
--- a/lib/ci/ansi2html.rb
+++ /dev/null
@@ -1,331 +0,0 @@
-# ANSI color library
-#
-# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
-module Ci
- module Ansi2html
- # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
- COLOR = {
- 0 => 'black', # not that this is gray in the intense color table
- 1 => 'red',
- 2 => 'green',
- 3 => 'yellow',
- 4 => 'blue',
- 5 => 'magenta',
- 6 => 'cyan',
- 7 => 'white', # not that this is gray in the dark (aka default) color table
- }.freeze
-
- STYLE_SWITCHES = {
- bold: 0x01,
- italic: 0x02,
- underline: 0x04,
- conceal: 0x08,
- cross: 0x10
- }.freeze
-
- def self.convert(ansi, state = nil)
- Converter.new.convert(ansi, state)
- end
-
- class Converter
- def on_0(s) reset() end
-
- def on_1(s) enable(STYLE_SWITCHES[:bold]) end
-
- def on_3(s) enable(STYLE_SWITCHES[:italic]) end
-
- def on_4(s) enable(STYLE_SWITCHES[:underline]) end
-
- def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
-
- def on_9(s) enable(STYLE_SWITCHES[:cross]) end
-
- def on_21(s) disable(STYLE_SWITCHES[:bold]) end
-
- def on_22(s) disable(STYLE_SWITCHES[:bold]) end
-
- def on_23(s) disable(STYLE_SWITCHES[:italic]) end
-
- def on_24(s) disable(STYLE_SWITCHES[:underline]) end
-
- def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
-
- def on_29(s) disable(STYLE_SWITCHES[:cross]) end
-
- def on_30(s) set_fg_color(0) end
-
- def on_31(s) set_fg_color(1) end
-
- def on_32(s) set_fg_color(2) end
-
- def on_33(s) set_fg_color(3) end
-
- def on_34(s) set_fg_color(4) end
-
- def on_35(s) set_fg_color(5) end
-
- def on_36(s) set_fg_color(6) end
-
- def on_37(s) set_fg_color(7) end
-
- def on_38(s) set_fg_color_256(s) end
-
- def on_39(s) set_fg_color(9) end
-
- def on_40(s) set_bg_color(0) end
-
- def on_41(s) set_bg_color(1) end
-
- def on_42(s) set_bg_color(2) end
-
- def on_43(s) set_bg_color(3) end
-
- def on_44(s) set_bg_color(4) end
-
- def on_45(s) set_bg_color(5) end
-
- def on_46(s) set_bg_color(6) end
-
- def on_47(s) set_bg_color(7) end
-
- def on_48(s) set_bg_color_256(s) end
-
- def on_49(s) set_bg_color(9) end
-
- def on_90(s) set_fg_color(0, 'l') end
-
- def on_91(s) set_fg_color(1, 'l') end
-
- def on_92(s) set_fg_color(2, 'l') end
-
- def on_93(s) set_fg_color(3, 'l') end
-
- def on_94(s) set_fg_color(4, 'l') end
-
- def on_95(s) set_fg_color(5, 'l') end
-
- def on_96(s) set_fg_color(6, 'l') end
-
- def on_97(s) set_fg_color(7, 'l') end
-
- def on_99(s) set_fg_color(9, 'l') end
-
- def on_100(s) set_bg_color(0, 'l') end
-
- def on_101(s) set_bg_color(1, 'l') end
-
- def on_102(s) set_bg_color(2, 'l') end
-
- def on_103(s) set_bg_color(3, 'l') end
-
- def on_104(s) set_bg_color(4, 'l') end
-
- def on_105(s) set_bg_color(5, 'l') end
-
- def on_106(s) set_bg_color(6, 'l') end
-
- def on_107(s) set_bg_color(7, 'l') end
-
- def on_109(s) set_bg_color(9, 'l') end
-
- attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
-
- STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
-
- def convert(stream, new_state)
- reset_state
- restore_state(new_state, stream) if new_state.present?
-
- append = false
- truncated = false
-
- cur_offset = stream.tell
- if cur_offset > @offset
- @offset = cur_offset
- truncated = true
- else
- stream.seek(@offset)
- append = @offset > 0
- end
- start_offset = @offset
-
- open_new_tag
-
- stream.each_line do |line|
- s = StringScanner.new(line)
- until s.eos?
- if s.scan(/\e([@-_])(.*?)([@-~])/)
- handle_sequence(s)
- elsif s.scan(/\e(([@-_])(.*?)?)?$/)
- break
- elsif s.scan(/</)
- @out << '&lt;'
- elsif s.scan(/\r?\n/)
- @out << '<br>'
- else
- @out << s.scan(/./m)
- end
- @offset += s.matched_size
- end
- end
-
- close_open_tags()
-
- OpenStruct.new(
- html: @out.force_encoding(Encoding.default_external),
- state: state,
- append: append,
- truncated: truncated,
- offset: start_offset,
- size: stream.tell - start_offset,
- total: stream.size
- )
- end
-
- def handle_sequence(s)
- indicator = s[1]
- commands = s[2].split ';'
- terminator = s[3]
-
- # We are only interested in color and text style changes - triggered by
- # sequences starting with '\e[' and ending with 'm'. Any other control
- # sequence gets stripped (including stuff like "delete last line")
- return unless indicator == '[' && terminator == 'm'
-
- close_open_tags()
-
- if commands.empty?()
- reset()
- return
- end
-
- evaluate_command_stack(commands)
-
- open_new_tag
- end
-
- def evaluate_command_stack(stack)
- return unless command = stack.shift()
-
- if self.respond_to?("on_#{command}", true)
- self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- evaluate_command_stack(stack)
- end
-
- def open_new_tag
- css_classes = []
-
- unless @fg_color.nil?
- fg_color = @fg_color
- # Most terminals show bold colored text in the light color variant
- # Let's mimic that here
- if @style_mask & STYLE_SWITCHES[:bold] != 0
- fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
- end
- css_classes << fg_color
- end
- css_classes << @bg_color unless @bg_color.nil?
-
- STYLE_SWITCHES.each do |css_class, flag|
- css_classes << "term-#{css_class}" if @style_mask & flag != 0
- end
-
- return if css_classes.empty?
-
- @out << %{<span class="#{css_classes.join(' ')}">}
- @n_open_tags += 1
- end
-
- def close_open_tags
- while @n_open_tags > 0
- @out << %{</span>}
- @n_open_tags -= 1
- end
- end
-
- def reset_state
- @offset = 0
- @n_open_tags = 0
- @out = ''
- reset
- end
-
- def state
- state = STATE_PARAMS.inject({}) do |h, param|
- h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
- h
- end
- Base64.urlsafe_encode64(state.to_json)
- end
-
- def restore_state(new_state, stream)
- state = Base64.urlsafe_decode64(new_state)
- state = JSON.parse(state, symbolize_names: true)
- return if state[:offset].to_i > stream.size
-
- STATE_PARAMS.each do |param|
- send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def reset
- @fg_color = nil
- @bg_color = nil
- @style_mask = 0
- end
-
- def enable(flag)
- @style_mask |= flag
- end
-
- def disable(flag)
- @style_mask &= ~flag
- end
-
- def set_fg_color(color_index, prefix = nil)
- @fg_color = get_term_color_class(color_index, ["fg", prefix])
- end
-
- def set_bg_color(color_index, prefix = nil)
- @bg_color = get_term_color_class(color_index, ["bg", prefix])
- end
-
- def get_term_color_class(color_index, prefix)
- color_name = COLOR[color_index]
- return nil if color_name.nil?
-
- get_color_class(["term", prefix, color_name])
- end
-
- def set_fg_color_256(command_stack)
- css_class = get_xterm_color_class(command_stack, "fg")
- @fg_color = css_class unless css_class.nil?
- end
-
- def set_bg_color_256(command_stack)
- css_class = get_xterm_color_class(command_stack, "bg")
- @bg_color = css_class unless css_class.nil?
- end
-
- def get_xterm_color_class(command_stack, prefix)
- # the 38 and 48 commands have to be followed by "5" and the color index
- return unless command_stack.length >= 2
- return unless command_stack[0] == "5"
-
- command_stack.shift() # ignore the "5" command
- color_index = command_stack.shift().to_i
-
- return unless color_index >= 0
- return unless color_index <= 255
-
- get_color_class(["xterm", prefix, color_index])
- end
-
- def get_color_class(segments)
- [segments].flatten.compact.join('-')
- end
- end
- end
-end
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
deleted file mode 100644
index 76a69bf8a83..00000000000
--- a/lib/ci/charts.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module Ci
- module Charts
- module DailyInterval
- def grouped_count(query)
- query
- .group("DATE(#{Ci::Pipeline.table_name}.created_at)")
- .count(:created_at)
- .transform_keys { |date| date.strftime(@format) }
- end
-
- def interval_step
- @interval_step ||= 1.day
- end
- end
-
- module MonthlyInterval
- def grouped_count(query)
- if Gitlab::Database.postgresql?
- query
- .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
- .count(:created_at)
- .transform_keys(&:squish)
- else
- query
- .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
- .count(:created_at)
- end
- end
-
- def interval_step
- @interval_step ||= 1.month
- end
- end
-
- class Chart
- attr_reader :labels, :total, :success, :project, :pipeline_times
-
- def initialize(project)
- @labels = []
- @total = []
- @success = []
- @pipeline_times = []
- @project = project
-
- collect
- end
-
- def collect
- query = project.pipelines
- .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
-
- totals_count = grouped_count(query)
- success_count = grouped_count(query.success)
-
- current = @from
- while current < @to
- label = current.strftime(@format)
-
- @labels << label
- @total << (totals_count[label] || 0)
- @success << (success_count[label] || 0)
-
- current += interval_step
- end
- end
- end
-
- class YearChart < Chart
- include MonthlyInterval
-
- def initialize(*)
- @to = Date.today.end_of_month
- @from = @to.years_ago(1).beginning_of_month
- @format = '%d %B %Y'
-
- super
- end
- end
-
- class MonthChart < Chart
- include DailyInterval
-
- def initialize(*)
- @to = Date.today
- @from = @to - 30.days
- @format = '%d %B'
-
- super
- end
- end
-
- class WeekChart < Chart
- include DailyInterval
-
- def initialize(*)
- @to = Date.today
- @from = @to - 7.days
- @format = '%d %B'
-
- super
- end
- end
-
- class PipelineTime < Chart
- def collect
- commits = project.pipelines.last(30)
-
- commits.each do |commit|
- @labels << commit.short_sha
- duration = commit.duration || 0
- @pipeline_times << (duration / 60)
- end
- end
- end
- end
-end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
deleted file mode 100644
index 62b44389b15..00000000000
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ /dev/null
@@ -1,251 +0,0 @@
-module Ci
- class GitlabCiYamlProcessor
- ValidationError = Class.new(StandardError)
-
- include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
-
- attr_reader :path, :cache, :stages, :jobs
-
- def initialize(config, path = nil)
- @ci_config = Gitlab::Ci::Config.new(config)
- @config = @ci_config.to_hash
- @path = path
-
- unless @ci_config.valid?
- raise ValidationError, @ci_config.errors.first
- end
-
- initial_parsing
- rescue Gitlab::Ci::Config::Loader::FormatError => e
- raise ValidationError, e.message
- end
-
- def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
- build_attributes(name)
- end
- end
-
- def builds
- @jobs.map do |name, _|
- build_attributes(name)
- end
- end
-
- def stage_seeds(pipeline)
- seeds = @stages.uniq.map do |stage|
- builds = pipeline_stage_builds(stage, pipeline)
-
- Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
- end
-
- seeds.compact
- end
-
- def build_attributes(name)
- job = @jobs[name.to_sym] || {}
-
- { stage_idx: @stages.index(job[:stage]),
- stage: job[:stage],
- commands: job[:commands],
- tag_list: job[:tags] || [],
- name: job[:name].to_s,
- allow_failure: job[:ignore],
- when: job[:when] || 'on_success',
- environment: job[:environment_name],
- coverage_regex: job[:coverage],
- yaml_variables: yaml_variables(name),
- options: {
- image: job[:image],
- services: job[:services],
- artifacts: job[:artifacts],
- cache: job[:cache],
- dependencies: job[:dependencies],
- before_script: job[:before_script],
- script: job[:script],
- after_script: job[:after_script],
- environment: job[:environment],
- retry: job[:retry]
- }.compact }
- end
-
- def self.validation_message(content)
- return 'Please provide content of .gitlab-ci.yml' if content.blank?
-
- begin
- Ci::GitlabCiYamlProcessor.new(content)
- nil
- rescue ValidationError, Psych::SyntaxError => e
- e.message
- end
- end
-
- private
-
- def pipeline_stage_builds(stage, pipeline)
- builds = builds_for_stage_and_ref(
- stage, pipeline.ref, pipeline.tag?, pipeline.source)
-
- builds.select do |build|
- job = @jobs[build.fetch(:name).to_sym]
- has_kubernetes = pipeline.has_kubernetes_active?
- only_kubernetes = job.dig(:only, :kubernetes)
- except_kubernetes = job.dig(:except, :kubernetes)
-
- [!only_kubernetes && !except_kubernetes,
- only_kubernetes && has_kubernetes,
- except_kubernetes && !has_kubernetes].any?
- end
- end
-
- def jobs_for_ref(ref, tag = false, source = nil)
- @jobs.select do |_, job|
- process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
- end
- end
-
- def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_ref(ref, tag, source).select do |_, job|
- job[:stage] == stage
- end
- end
-
- def initial_parsing
- ##
- # Global config
- #
- @before_script = @ci_config.before_script
- @image = @ci_config.image
- @after_script = @ci_config.after_script
- @services = @ci_config.services
- @variables = @ci_config.variables
- @stages = @ci_config.stages
- @cache = @ci_config.cache
-
- ##
- # Jobs
- #
- @jobs = @ci_config.jobs
-
- @jobs.each do |name, job|
- # logical validation for job
-
- validate_job_stage!(name, job)
- validate_job_dependencies!(name, job)
- validate_job_environment!(name, job)
- end
- end
-
- def yaml_variables(name)
- variables = (@variables || {})
- .merge(job_variables(name))
-
- variables.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
- end
-
- def job_variables(name)
- job = @jobs[name.to_sym]
- return {} unless job
-
- job[:variables] || {}
- end
-
- def validate_job_stage!(name, job)
- return unless job[:stage]
-
- unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
- raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
- end
- end
-
- def validate_job_dependencies!(name, job)
- return unless job[:dependencies]
-
- stage_index = @stages.index(job[:stage])
-
- job[:dependencies].each do |dependency|
- raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
-
- unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
- raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
- end
- end
- end
-
- def validate_job_environment!(name, job)
- return unless job[:environment]
- return unless job[:environment].is_a?(Hash)
-
- environment = job[:environment]
- validate_on_stop_job!(name, environment, environment[:on_stop])
- end
-
- def validate_on_stop_job!(name, environment, on_stop)
- return unless on_stop
-
- on_stop_job = @jobs[on_stop.to_sym]
- unless on_stop_job
- raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
- end
-
- unless on_stop_job[:environment]
- raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
- end
-
- unless on_stop_job[:environment][:name] == environment[:name]
- raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
- end
-
- unless on_stop_job[:environment][:action] == 'stop'
- raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
- end
- end
-
- def process?(only_params, except_params, ref, tag, source)
- if only_params.present?
- return false unless matching?(only_params, ref, tag, source)
- end
-
- if except_params.present?
- return false if matching?(except_params, ref, tag, source)
- end
-
- true
- end
-
- def matching?(patterns, ref, tag, source)
- patterns.any? do |pattern|
- pattern, path = pattern.split('@', 2)
- matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
- end
- end
-
- def matches_path?(path)
- return true unless path
-
- path == self.path
- end
-
- def matches_pattern?(pattern, ref, tag, source)
- return true if tag && pattern == 'tags'
- return true if !tag && pattern == 'branches'
- return true if source_to_pattern(source) == pattern
-
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ ref
- else
- pattern == ref
- end
- end
-
- def source_to_pattern(source)
- if %w[api external web].include?(source)
- source
- else
- source&.pluralize
- end
- end
- end
-end
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
deleted file mode 100644
index 997377abc55..00000000000
--- a/lib/ci/mask_secret.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Ci::MaskSecret
- class << self
- def mask!(value, token)
- return value unless value.present? && token.present?
-
- value.gsub!(token, 'x' * token.length)
- value
- end
- end
-end
diff --git a/lib/ci/model.rb b/lib/ci/model.rb
deleted file mode 100644
index c42a0ad36db..00000000000
--- a/lib/ci/model.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Ci
- module Model
- def table_name_prefix
- "ci_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
-end
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index bfcec241489..7cfa82a9a9f 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -206,11 +206,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return false if passes.any? { |p| p == false }
- return true if passes.all? { |p| p == true }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == false
+ end
+
+ true
end
def repr
@@ -245,11 +247,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return true if passes.any? { |p| p == true }
- return false if passes.all? { |p| p == false }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == true
+ end
+
+ false
end
def score(context)
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 56afd1f1392..45ff2ef9ced 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -107,7 +107,7 @@ module DeclarativePolicy
end
# This is the core spot where all those `#score` methods matter.
- # It is critcal for performance to run steps in the correct order,
+ # It is critical for performance to run steps in the correct order,
# so that we don't compute expensive conditions (potentially n times
# if we're called on, say, a large list of users).
#
@@ -139,30 +139,39 @@ module DeclarativePolicy
return
end
- steps = Set.new(@steps)
- remaining_enablers = steps.count { |s| s.enable? }
+ remaining_steps = Set.new(@steps)
+ remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
loop do
- return if steps.empty?
+ if @state.enabled?
+ # Once we set this, we never need to unset it, because a single
+ # prevent will stop this from being enabled
+ remaining_steps = remaining_preventers
+ else
+ # if the permission hasn't yet been enabled and we only have
+ # prevent steps left, we short-circuit the state here
+ @state.prevent! if remaining_enablers.empty?
+ end
- # if the permission hasn't yet been enabled and we only have
- # prevent steps left, we short-circuit the state here
- @state.prevent! if !@state.enabled? && remaining_enablers == 0
+ return if remaining_steps.empty?
lowest_score = Float::INFINITY
next_step = nil
- steps.each do |step|
+ remaining_steps.each do |step|
score = step.score
+
if score < lowest_score
next_step = step
lowest_score = score
end
- end
- steps.delete(next_step)
+ break if lowest_score.zero?
+ end
- remaining_enablers -= 1 if next_step.enable?
+ [remaining_steps, remaining_enablers, remaining_preventers].each do |set|
+ set.delete(next_step)
+ end
yield next_step, lowest_score
end
diff --git a/lib/github/client.rb b/lib/github/client.rb
index 9c476df7d46..29bd9c1f39e 100644
--- a/lib/github/client.rb
+++ b/lib/github/client.rb
@@ -1,6 +1,7 @@
module Github
class Client
TIMEOUT = 60
+ DEFAULT_PER_PAGE = 100
attr_reader :connection, :rate_limit
@@ -20,7 +21,7 @@ module Github
exceed, reset_in = rate_limit.get
sleep reset_in if exceed
- Github::Response.new(connection.get(url, query))
+ Github::Response.new(connection.get(url, { per_page: DEFAULT_PER_PAGE }.merge(query)))
end
private
diff --git a/lib/github/import.rb b/lib/github/import.rb
index 9354e142d3d..55f8387f27a 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -1,48 +1,15 @@
require_relative 'error'
+require_relative 'import/issue'
+require_relative 'import/legacy_diff_note'
+require_relative 'import/merge_request'
+require_relative 'import/note'
module Github
class Import
include Gitlab::ShellAdapter
- class MergeRequest < ::MergeRequest
- self.table_name = 'merge_requests'
-
- self.reset_callbacks :create
- 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 :create
- 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, :repo_url, :wiki_url,
- :options, :errors, :cached, :verbose
+ :options, :errors, :cached, :verbose, :last_fetched_at
def initialize(project, options = {})
@project = project
@@ -54,12 +21,13 @@ module Github
@verbose = options.fetch(:verbose, false)
@cached = Hash.new { |hash, key| hash[key] = Hash.new }
@errors = []
+ @last_fetched_at = nil
end
# rubocop: disable Rails/Output
def execute
puts 'Fetching repository...'.color(:aqua) if verbose
- fetch_repository
+ setup_and_fetch_repository
puts 'Fetching labels...'.color(:aqua) if verbose
fetch_labels
puts 'Fetching milestones...'.color(:aqua) if verbose
@@ -75,7 +43,7 @@ module Github
puts 'Expiring repository cache...'.color(:aqua) if verbose
expire_repository_cache
- true
+ errors.empty?
rescue Github::RepositoryFetchError
expire_repository_cache
false
@@ -85,18 +53,24 @@ module Github
private
- def fetch_repository
+ def setup_and_fetch_repository
begin
project.ensure_repository
project.repository.add_remote('github', repo_url)
- project.repository.set_remote_as_mirror('github')
- project.repository.fetch_remote('github', forced: true)
+ project.repository.set_import_remote_as_mirror('github')
+ project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
+ fetch_remote(forced: true)
rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
error(:project, repo_url, e.message)
raise Github::RepositoryFetchError
end
end
+ def fetch_remote(forced: false)
+ @last_fetched_at = Time.now
+ project.repository.fetch_remote('github', forced: forced)
+ end
+
def fetch_wiki_repository
return if project.wiki.repository_exists?
@@ -125,7 +99,7 @@ module Github
label.color = representation.color
end
- cached[:label_ids][label.title] = label.id
+ cached[:label_ids][representation.title] = label.id
rescue => e
error(:label, representation.url, e.message)
end
@@ -176,7 +150,9 @@ module Github
next unless merge_request.new_record? && pull_request.valid?
begin
- pull_request.restore_branches!
+ # If the PR has been created/updated after we last fetched the
+ # remote, we fetch again to get the up-to-date refs.
+ fetch_remote if pull_request.updated_at > last_fetched_at
author_id = user_id(pull_request.author, project.creator_id)
description = format_description(pull_request.description, pull_request.author)
@@ -185,6 +161,7 @@ module Github
iid: pull_request.iid,
title: pull_request.title,
description: description,
+ ref_fetched: true,
source_project: pull_request.source_project,
source_branch: pull_request.source_branch_name,
source_branch_sha: pull_request.source_branch_sha,
@@ -202,17 +179,10 @@ module Github
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
- pull_request.remove_restored_branches!
end
end
@@ -241,12 +211,17 @@ module Github
# for both features, like manipulating assignees, labels
# and milestones, are provided within the Issues API.
if representation.pull_request?
- return unless representation.has_labels?
+ return unless representation.labels? || representation.comments?
merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
- merge_request.update_attribute(:label_ids, label_ids(representation.labels))
+
+ if representation.labels?
+ merge_request.update_attribute(:label_ids, label_ids(representation.labels))
+ end
+
+ fetch_comments_conditionally(merge_request, representation)
else
- return if Issue.where(iid: representation.iid, project_id: project.id).exists?
+ return if Issue.exists?(iid: representation.iid, project_id: project.id)
author_id = user_id(representation.author, project.creator_id)
issue = Issue.new
@@ -255,25 +230,30 @@ module Github
issue.title = representation.title
issue.description = format_description(representation.description, representation.author)
issue.state = representation.state
- issue.label_ids = label_ids(representation.labels)
issue.milestone_id = milestone_id(representation.milestone)
issue.author_id = author_id
- issue.assignee_ids = [user_id(representation.assignee)]
issue.created_at = representation.created_at
issue.updated_at = representation.updated_at
issue.save!(validate: false)
- # Fetch comments
- if representation.has_comments?
- comments_url = "/repos/#{repo}/issues/#{issue.iid}/comments"
- fetch_comments(issue, :comment, comments_url)
- end
+ issue.update(
+ label_ids: label_ids(representation.labels),
+ assignee_ids: assignee_ids(representation.assignees))
+
+ fetch_comments_conditionally(issue, representation)
end
rescue => e
error(:issue, representation.url, e.message)
end
end
+ def fetch_comments_conditionally(issuable, representation)
+ if representation.comments?
+ comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments"
+ fetch_comments(issuable, :comment, comments_url)
+ end
+ end
+
def fetch_comments(noteable, type, url, klass = Note)
while url
comments = Github::Client.new(options).get(url)
@@ -332,7 +312,11 @@ module Github
end
def label_ids(labels)
- labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact
+ labels.map { |label| cached[:label_ids][label.title] }.compact
+ end
+
+ def assignee_ids(assignees)
+ assignees.map { |assignee| user_id(assignee) }.compact
end
def milestone_id(milestone)
diff --git a/lib/github/import/issue.rb b/lib/github/import/issue.rb
new file mode 100644
index 00000000000..171f0872666
--- /dev/null
+++ b/lib/github/import/issue.rb
@@ -0,0 +1,13 @@
+module Github
+ class Import
+ class Issue < ::Issue
+ self.table_name = 'issues'
+
+ self.reset_callbacks :save
+ self.reset_callbacks :create
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+ end
+end
diff --git a/lib/github/import/legacy_diff_note.rb b/lib/github/import/legacy_diff_note.rb
new file mode 100644
index 00000000000..18adff560b6
--- /dev/null
+++ b/lib/github/import/legacy_diff_note.rb
@@ -0,0 +1,12 @@
+module Github
+ class Import
+ class LegacyDiffNote < ::LegacyDiffNote
+ self.table_name = 'notes'
+ self.store_full_sti_class = false
+
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+ end
+end
diff --git a/lib/github/import/merge_request.rb b/lib/github/import/merge_request.rb
new file mode 100644
index 00000000000..c258e5d5e0e
--- /dev/null
+++ b/lib/github/import/merge_request.rb
@@ -0,0 +1,13 @@
+module Github
+ class Import
+ class MergeRequest < ::MergeRequest
+ self.table_name = 'merge_requests'
+
+ self.reset_callbacks :create
+ self.reset_callbacks :save
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+ end
+end
diff --git a/lib/github/import/note.rb b/lib/github/import/note.rb
new file mode 100644
index 00000000000..8cf4f30e6b7
--- /dev/null
+++ b/lib/github/import/note.rb
@@ -0,0 +1,13 @@
+module Github
+ class Import
+ class Note < ::Note
+ self.table_name = 'notes'
+ self.store_full_sti_class = false
+
+ self.reset_callbacks :save
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+ end
+end
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
index 823e8e9a9c4..0087a3d3c4f 100644
--- a/lib/github/representation/branch.rb
+++ b/lib/github/representation/branch.rb
@@ -7,10 +7,14 @@ module Github
raw.dig('user', 'login') || 'unknown'
end
+ def repo?
+ raw['repo'].present?
+ end
+
def repo
- return @repo if defined?(@repo)
+ return unless repo?
- @repo = Github::Representation::Repo.new(raw['repo']) if raw['repo'].present?
+ @repo ||= Github::Representation::Repo.new(raw['repo'])
end
def ref
@@ -25,10 +29,6 @@ module Github
Commit.truncate_sha(sha)
end
- def exists?
- @exists ||= branch_exists? && commit_exists?
- end
-
def valid?
sha.present? && ref.present?
end
@@ -47,14 +47,6 @@ module Github
private
- def branch_exists?
- repository.branch_exists?(ref)
- end
-
- def commit_exists?
- repository.branch_names_contains(sha).include?(ref)
- end
-
def repository
@repository ||= options.fetch(:repository)
end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
index 9713b82615d..768ba3b993c 100644
--- a/lib/github/representation/issuable.rb
+++ b/lib/github/representation/issuable.rb
@@ -23,14 +23,14 @@ module Github
@author ||= Github::Representation::User.new(raw['user'], options)
end
- def assignee
- return unless assigned?
-
- @assignee ||= Github::Representation::User.new(raw['assignee'], options)
+ def labels?
+ raw['labels'].any?
end
- def assigned?
- raw['assignee'].present?
+ def labels
+ @labels ||= Array(raw['labels']).map do |label|
+ Github::Representation::Label.new(label, options)
+ end
end
end
end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
index df3540a6e6c..4f1a02cb90f 100644
--- a/lib/github/representation/issue.rb
+++ b/lib/github/representation/issue.rb
@@ -1,25 +1,27 @@
module Github
module Representation
class Issue < Representation::Issuable
- def labels
- raw['labels']
- end
-
def state
raw['state'] == 'closed' ? 'closed' : 'opened'
end
- def has_comments?
+ def comments?
raw['comments'] > 0
end
- def has_labels?
- labels.count > 0
- end
-
def pull_request?
raw['pull_request'].present?
end
+
+ def assigned?
+ raw['assignees'].present?
+ end
+
+ def assignees
+ @assignees ||= Array(raw['assignees']).map do |user|
+ Github::Representation::User.new(user, options)
+ end
+ end
end
end
end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
index 55461097e8a..0171179bb0f 100644
--- a/lib/github/representation/pull_request.rb
+++ b/lib/github/representation/pull_request.rb
@@ -1,26 +1,17 @@
module Github
module Representation
class PullRequest < Representation::Issuable
- delegate :user, :repo, :ref, :sha, to: :source_branch, prefix: true
- delegate :user, :exists?, :repo, :ref, :sha, :short_sha, to: :target_branch, prefix: true
+ delegate :sha, to: :source_branch, prefix: true
+ delegate :sha, to: :target_branch, prefix: true
def source_project
project
end
def source_branch_name
- @source_branch_name ||=
- if cross_project? || !source_branch_exists?
- source_branch_name_prefixed
- else
- source_branch_ref
- end
- end
-
- def source_branch_exists?
- return @source_branch_exists if defined?(@source_branch_exists)
-
- @source_branch_exists = !cross_project? && source_branch.exists?
+ # Mimic the "user:branch" displayed in the MR widget,
+ # i.e. "Request to merge rymai:add-external-mounts into master"
+ cross_project? ? "#{source_branch.user}:#{source_branch.ref}" : source_branch.ref
end
def target_project
@@ -28,11 +19,7 @@ module Github
end
def target_branch_name
- @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
- end
-
- def target_branch_exists?
- @target_branch_exists ||= target_branch.exists?
+ target_branch.ref
end
def state
@@ -50,16 +37,14 @@ module Github
source_branch.valid? && target_branch.valid?
end
- def restore_branches!
- restore_source_branch!
- restore_target_branch!
+ def assigned?
+ raw['assignee'].present?
end
- def remove_restored_branches!
- return if opened?
+ def assignee
+ return unless assigned?
- remove_source_branch!
- remove_target_branch!
+ @assignee ||= Github::Representation::User.new(raw['assignee'], options)
end
private
@@ -72,48 +57,14 @@ module Github
@source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
end
- def source_branch_name_prefixed
- "gh-#{target_branch_short_sha}/#{iid}/#{source_branch_user}/#{source_branch_ref}"
- end
-
def target_branch
@target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
end
- def target_branch_name_prefixed
- "gl-#{target_branch_short_sha}/#{iid}/#{target_branch_user}/#{target_branch_ref}"
- end
-
def cross_project?
- return true if source_branch_repo.nil?
-
- source_branch_repo.id != target_branch_repo.id
- end
-
- def restore_source_branch!
- return if source_branch_exists?
-
- source_branch.restore!(source_branch_name)
- end
-
- def restore_target_branch!
- return if target_branch_exists?
-
- target_branch.restore!(target_branch_name)
- end
-
- def remove_source_branch!
- # We should remove the source/target branches only if they were
- # restored. Otherwise, we'll remove branches like 'master' that
- # target_branch_exists? returns true. In other words, we need
- # to clean up only the restored branches that (source|target)_branch_exists?
- # returns false for the first time it has been called, because of
- # this that is important to memoize these values.
- source_branch.remove!(source_branch_name) unless source_branch_exists?
- end
+ return true unless source_branch.repo?
- def remove_target_branch!
- target_branch.remove!(target_branch_name) unless target_branch_exists?
+ source_branch.repo.id != target_branch.repo.id
end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 3fd81759d25..87aeb76b66a 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -13,11 +13,6 @@ module Gitlab
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
- AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze
-
- # Other available scopes
- OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
-
class << self
include Gitlab::CurrentSettings
@@ -132,7 +127,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
- if token && valid_scoped_token?(token, AVAILABLE_SCOPES)
+ if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
end
end
@@ -230,6 +225,21 @@ module Gitlab
def read_user_scope_authentication_abilities
[]
end
+
+ def available_scopes
+ API_SCOPES + registry_scopes
+ end
+
+ # Other available scopes
+ def optional_scopes
+ available_scopes + OPENID_SCOPES - DEFAULT_SCOPES
+ end
+
+ def registry_scopes
+ return [] unless Gitlab.config.registry.enabled
+
+ REGISTRY_SCOPES
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
new file mode 100644
index 00000000000..c88eb9783ed
--- /dev/null
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ module BackgroundMigration
+ class CreateForkNetworkMembershipsRange
+ RESCHEDULE_DELAY = 15
+
+ class ForkedProjectLink < ActiveRecord::Base
+ self.table_name = 'forked_project_links'
+ end
+
+ def perform(start_id, end_id)
+ log("Creating memberships for forks: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS
+ INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
+
+ SELECT fork_network_members.fork_network_id,
+ forked_project_links.forked_to_project_id,
+ forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ INNER JOIN fork_network_members
+ ON forked_project_links.forked_from_project_id = fork_network_members.project_id
+
+ WHERE forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_network_members existing_members
+ WHERE existing_members.project_id = forked_project_links.forked_to_project_id
+ )
+ INSERT_MEMBERS
+
+ if missing_members?(start_id, end_id)
+ BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+ end
+
+ def missing_members?(start_id, end_id)
+ count_sql = <<~MISSING_MEMBERS
+ SELECT COUNT(*)
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE forked_project_links.forked_from_project_id = projects.id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ MISSING_MEMBERS
+
+ ForkNetworkMember.count_by_sql(count_sql) > 0
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
new file mode 100644
index 00000000000..e94719db72e
--- /dev/null
+++ b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
@@ -0,0 +1,53 @@
+class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
+ class GpgKey < ActiveRecord::Base
+ self.table_name = 'gpg_keys'
+
+ include EachBatch
+ include ShaAttribute
+
+ sha_attribute :primary_keyid
+ sha_attribute :fingerprint
+
+ has_many :subkeys, class_name: 'GpgKeySubkey'
+ end
+
+ class GpgKeySubkey < ActiveRecord::Base
+ self.table_name = 'gpg_key_subkeys'
+
+ include ShaAttribute
+
+ sha_attribute :keyid
+ sha_attribute :fingerprint
+ end
+
+ def perform(gpg_key_id)
+ gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return if gpg_key.nil?
+ return if gpg_key.subkeys.any?
+
+ create_subkeys(gpg_key)
+ update_signatures(gpg_key)
+ end
+
+ private
+
+ def create_subkeys(gpg_key)
+ gpg_subkeys = Gitlab::Gpg.subkeys_from_key(gpg_key.key)
+
+ gpg_subkeys[gpg_key.primary_keyid.upcase]&.each do |subkey_data|
+ gpg_key.subkeys.build(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
+ end
+
+ # Improve latency by doing all INSERTs in a single call
+ GpgKey.transaction do
+ gpg_key.save!
+ end
+ end
+
+ def update_signatures(gpg_key)
+ return unless gpg_key.subkeys.exists?
+
+ InvalidGpgSignatureUpdateWorker.perform_async(gpg_key.id)
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
new file mode 100644
index 00000000000..b1411be3016
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module BackgroundMigration
+ class DeleteConflictingRedirectRoutesRange
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+ end
+
+ class RedirectRoute < ActiveRecord::Base
+ self.table_name = 'redirect_routes'
+ end
+
+ # start_id - The start ID of the range of events to process
+ # end_id - The end ID of the range to process.
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id))
+ num_rows = conflicts.delete_all
+
+ Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.")
+ end
+
+ def migrate?
+ Route.table_exists? && RedirectRoute.table_exists?
+ end
+
+ def routes_match_redirects_clause(start_id, end_id)
+ <<~ROUTES_MATCH_REDIRECTS
+ EXISTS (
+ SELECT 1 FROM routes
+ WHERE (
+ LOWER(redirect_routes.path) = LOWER(routes.path)
+ OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%'))
+ )
+ AND routes.id BETWEEN #{start_id} AND #{end_id}
+ )
+ ROUTES_MATCH_REDIRECTS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
index 3fde1b09efb..8e5c95f2287 100644
--- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
+++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
@@ -3,11 +3,18 @@ module Gitlab
class DeserializeMergeRequestDiffsAndCommits
attr_reader :diff_ids, :commit_rows, :file_rows
+ class Error < StandardError
+ def backtrace
+ cause.backtrace
+ end
+ end
+
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
end
BUFFER_ROWS = 1000
+ DIFF_FILE_BUFFER_ROWS = 100
def perform(start_id, stop_id)
merge_request_diffs = MergeRequestDiff
@@ -26,13 +33,17 @@ module Gitlab
if diff_ids.length > BUFFER_ROWS ||
commit_rows.length > BUFFER_ROWS ||
- file_rows.length > BUFFER_ROWS
+ file_rows.length > DIFF_FILE_BUFFER_ROWS
flush_buffers!
end
end
flush_buffers!
+ rescue => e
+ Rails.logger.info("#{self.class.name}: failed for IDs #{merge_request_diffs.map(&:id)} with #{e.class.name}")
+
+ raise Error.new(e.inspect)
end
private
@@ -45,17 +56,28 @@ module Gitlab
def flush_buffers!
if diff_ids.any?
- MergeRequestDiff.transaction do
- Gitlab::Database.bulk_insert('merge_request_diff_commits', commit_rows)
- Gitlab::Database.bulk_insert('merge_request_diff_files', file_rows)
+ commit_rows.each_slice(BUFFER_ROWS).each do |commit_rows_slice|
+ bulk_insert('merge_request_diff_commits', commit_rows_slice)
+ end
- MergeRequestDiff.where(id: diff_ids).update_all(st_commits: nil, st_diffs: nil)
+ file_rows.each_slice(DIFF_FILE_BUFFER_ROWS).each do |file_rows_slice|
+ bulk_insert('merge_request_diff_files', file_rows_slice)
end
+
+ MergeRequestDiff.where(id: diff_ids).update_all(st_commits: nil, st_diffs: nil)
end
reset_buffers!
end
+ def bulk_insert(table, rows)
+ Gitlab::Database.bulk_insert(table, rows)
+ rescue ActiveRecord::RecordNotUnique
+ ids = rows.map { |row| row[:merge_request_diff_id] }.uniq.sort
+
+ Rails.logger.info("#{self.class.name}: rows inserted twice for IDs #{ids}")
+ end
+
def single_diff_rows(merge_request_diff)
sha_attribute = Gitlab::Database::ShaAttribute.new
commits = YAML.load(merge_request_diff.st_commits) rescue []
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
new file mode 100644
index 00000000000..bc53e6d7f94
--- /dev/null
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -0,0 +1,313 @@
+module Gitlab
+ module BackgroundMigration
+ class NormalizeLdapExternUidsRange
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+ end
+
+ # Copied this class to make this migration resilient to future code changes.
+ # And if the normalize behavior is changed in the future, it must be
+ # accompanied by another migration.
+ module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+ end
+
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
+ ldap_identities.each do |identity|
+ begin
+ identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ unless identity.save
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
+ end
+ rescue Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
+ end
+ end
+ end
+
+ def migrate?
+ Identity.table_exists?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
new file mode 100644
index 00000000000..2ef3a207dd3
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module BackgroundMigration
+ class PopulateForkNetworksRange
+ def perform(start_id, end_id)
+ log("Creating fork networks for forked project links: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
+ INSERT INTO fork_networks (root_project_id)
+ SELECT DISTINCT forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM forked_project_links inner_links
+ WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ )
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_networks
+ WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_NETWORKS
+
+ log("Creating memberships for root projects: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_ROOT
+ INSERT INTO fork_network_members (fork_network_id, project_id)
+ SELECT DISTINCT fork_networks.id, fork_networks.root_project_id
+
+ FROM fork_networks
+
+ INNER JOIN forked_project_links
+ ON forked_project_links.forked_from_project_id = fork_networks.root_project_id
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = fork_networks.root_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_ROOT
+
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
+ BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_importer.rb b/lib/gitlab/bare_repository_importer.rb
index 9323bfc7fb2..1d98d187805 100644
--- a/lib/gitlab/bare_repository_importer.rb
+++ b/lib/gitlab/bare_repository_importer.rb
@@ -56,7 +56,8 @@ module Gitlab
name: project_path,
path: project_path,
repository_storage: storage_name,
- namespace_id: group&.id
+ namespace_id: group&.id,
+ skip_disk_validation: true
}
project = Projects::CreateService.new(user, project_params).execute
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 28bbf3b384e..d1979bb7ed3 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -149,16 +149,21 @@ module Gitlab
description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
description += pull_request.description
+ source_branch_sha = pull_request.source_branch_sha
+ target_branch_sha = pull_request.target_branch_sha
+ source_branch_sha = project.repository.commit(source_branch_sha)&.sha || source_branch_sha
+ target_branch_sha = project.repository.commit(target_branch_sha)&.sha || target_branch_sha
+
merge_request = project.merge_requests.create!(
iid: pull_request.iid,
title: pull_request.title,
description: description,
source_project: project,
source_branch: pull_request.source_branch_name,
- source_branch_sha: pull_request.source_branch_sha,
+ source_branch_sha: source_branch_sha,
target_project: project,
target_branch: pull_request.target_branch_name,
- target_branch_sha: pull_request.target_branch_sha,
+ target_branch_sha: target_branch_sha,
state: pull_request.state,
author_id: gitlab_user_id(project, pull_request.author),
assignee_id: nil,
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
new file mode 100644
index 00000000000..72b75791bbb
--- /dev/null
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -0,0 +1,344 @@
+# ANSI color library
+#
+# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
+module Gitlab
+ module Ci
+ module Ansi2html
+ # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
+ COLOR = {
+ 0 => 'black', # not that this is gray in the intense color table
+ 1 => 'red',
+ 2 => 'green',
+ 3 => 'yellow',
+ 4 => 'blue',
+ 5 => 'magenta',
+ 6 => 'cyan',
+ 7 => 'white', # not that this is gray in the dark (aka default) color table
+ }.freeze
+
+ STYLE_SWITCHES = {
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10
+ }.freeze
+
+ def self.convert(ansi, state = nil)
+ Converter.new.convert(ansi, state)
+ end
+
+ class Converter
+ def on_0(s) reset() end
+
+ def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+
+ def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+
+ def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
+ def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+
+ def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+
+ def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+
+ def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+
+ def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+
+ def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
+ def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+
+ def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+
+ def on_30(s) set_fg_color(0) end
+
+ def on_31(s) set_fg_color(1) end
+
+ def on_32(s) set_fg_color(2) end
+
+ def on_33(s) set_fg_color(3) end
+
+ def on_34(s) set_fg_color(4) end
+
+ def on_35(s) set_fg_color(5) end
+
+ def on_36(s) set_fg_color(6) end
+
+ def on_37(s) set_fg_color(7) end
+
+ def on_38(s) set_fg_color_256(s) end
+
+ def on_39(s) set_fg_color(9) end
+
+ def on_40(s) set_bg_color(0) end
+
+ def on_41(s) set_bg_color(1) end
+
+ def on_42(s) set_bg_color(2) end
+
+ def on_43(s) set_bg_color(3) end
+
+ def on_44(s) set_bg_color(4) end
+
+ def on_45(s) set_bg_color(5) end
+
+ def on_46(s) set_bg_color(6) end
+
+ def on_47(s) set_bg_color(7) end
+
+ def on_48(s) set_bg_color_256(s) end
+
+ def on_49(s) set_bg_color(9) end
+
+ def on_90(s) set_fg_color(0, 'l') end
+
+ def on_91(s) set_fg_color(1, 'l') end
+
+ def on_92(s) set_fg_color(2, 'l') end
+
+ def on_93(s) set_fg_color(3, 'l') end
+
+ def on_94(s) set_fg_color(4, 'l') end
+
+ def on_95(s) set_fg_color(5, 'l') end
+
+ def on_96(s) set_fg_color(6, 'l') end
+
+ def on_97(s) set_fg_color(7, 'l') end
+
+ def on_99(s) set_fg_color(9, 'l') end
+
+ def on_100(s) set_bg_color(0, 'l') end
+
+ def on_101(s) set_bg_color(1, 'l') end
+
+ def on_102(s) set_bg_color(2, 'l') end
+
+ def on_103(s) set_bg_color(3, 'l') end
+
+ def on_104(s) set_bg_color(4, 'l') end
+
+ def on_105(s) set_bg_color(5, 'l') end
+
+ def on_106(s) set_bg_color(6, 'l') end
+
+ def on_107(s) set_bg_color(7, 'l') end
+
+ def on_109(s) set_bg_color(9, 'l') end
+
+ attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
+
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
+
+ def convert(stream, new_state)
+ reset_state
+ restore_state(new_state, stream) if new_state.present?
+
+ append = false
+ truncated = false
+
+ cur_offset = stream.tell
+ if cur_offset > @offset
+ @offset = cur_offset
+ truncated = true
+ else
+ stream.seek(@offset)
+ append = @offset > 0
+ end
+ start_offset = @offset
+
+ open_new_tag
+
+ stream.each_line do |line|
+ s = StringScanner.new(line)
+ until s.eos?
+ if s.scan(Gitlab::Regex.build_trace_section_regex)
+ handle_section(s)
+ elsif s.scan(/\e([@-_])(.*?)([@-~])/)
+ handle_sequence(s)
+ elsif s.scan(/\e(([@-_])(.*?)?)?$/)
+ break
+ elsif s.scan(/</)
+ @out << '&lt;'
+ elsif s.scan(/\r?\n/)
+ @out << '<br>'
+ else
+ @out << s.scan(/./m)
+ end
+ @offset += s.matched_size
+ end
+ end
+
+ close_open_tags()
+
+ OpenStruct.new(
+ html: @out.force_encoding(Encoding.default_external),
+ state: state,
+ append: append,
+ truncated: truncated,
+ offset: start_offset,
+ size: stream.tell - start_offset,
+ total: stream.size
+ )
+ end
+
+ def handle_section(s)
+ action = s[1]
+ timestamp = s[2]
+ section = s[3]
+ line = s.matched()[0...-5] # strips \r\033[0K
+
+ @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
+ end
+
+ def handle_sequence(s)
+ indicator = s[1]
+ commands = s[2].split ';'
+ terminator = s[3]
+
+ # We are only interested in color and text style changes - triggered by
+ # sequences starting with '\e[' and ending with 'm'. Any other control
+ # sequence gets stripped (including stuff like "delete last line")
+ return unless indicator == '[' && terminator == 'm'
+
+ close_open_tags()
+
+ if commands.empty?()
+ reset()
+ return
+ end
+
+ evaluate_command_stack(commands)
+
+ open_new_tag
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag
+ css_classes = []
+
+ unless @fg_color.nil?
+ fg_color = @fg_color
+ # Most terminals show bold colored text in the light color variant
+ # Let's mimic that here
+ if @style_mask & STYLE_SWITCHES[:bold] != 0
+ fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
+ end
+ css_classes << fg_color
+ end
+ css_classes << @bg_color unless @bg_color.nil?
+
+ STYLE_SWITCHES.each do |css_class, flag|
+ css_classes << "term-#{css_class}" if @style_mask & flag != 0
+ end
+
+ return if css_classes.empty?
+
+ @out << %{<span class="#{css_classes.join(' ')}">}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{</span>}
+ @n_open_tags -= 1
+ end
+ end
+
+ def reset_state
+ @offset = 0
+ @n_open_tags = 0
+ @out = ''
+ reset
+ end
+
+ def state
+ state = STATE_PARAMS.inject({}) do |h, param|
+ h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
+ h
+ end
+ Base64.urlsafe_encode64(state.to_json)
+ end
+
+ def restore_state(new_state, stream)
+ state = Base64.urlsafe_decode64(new_state)
+ state = JSON.parse(state, symbolize_names: true)
+ return if state[:offset].to_i > stream.size
+
+ STATE_PARAMS.each do |param|
+ send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def reset
+ @fg_color = nil
+ @bg_color = nil
+ @style_mask = 0
+ end
+
+ def enable(flag)
+ @style_mask |= flag
+ end
+
+ def disable(flag)
+ @style_mask &= ~flag
+ end
+
+ def set_fg_color(color_index, prefix = nil)
+ @fg_color = get_term_color_class(color_index, ["fg", prefix])
+ end
+
+ def set_bg_color(color_index, prefix = nil)
+ @bg_color = get_term_color_class(color_index, ["bg", prefix])
+ end
+
+ def get_term_color_class(color_index, prefix)
+ color_name = COLOR[color_index]
+ return nil if color_name.nil?
+
+ get_color_class(["term", prefix, color_name])
+ end
+
+ def set_fg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "fg")
+ @fg_color = css_class unless css_class.nil?
+ end
+
+ def set_bg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "bg")
+ @bg_color = css_class unless css_class.nil?
+ end
+
+ def get_xterm_color_class(command_stack, prefix)
+ # the 38 and 48 commands have to be followed by "5" and the color index
+ return unless command_stack.length >= 2
+ return unless command_stack[0] == "5"
+
+ command_stack.shift() # ignore the "5" command
+ color_index = command_stack.shift().to_i
+
+ return unless color_index >= 0
+ return unless color_index <= 255
+
+ get_color_class(["xterm", prefix, color_index])
+ end
+
+ def get_color_class(segments)
+ [segments].flatten.compact.join('-')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb
new file mode 100644
index 00000000000..d10cc7802d4
--- /dev/null
+++ b/lib/gitlab/ci/build/policy.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ def self.fabricate(specs)
+ specifications = specs.to_h.map do |spec, value|
+ self.const_get(spec.to_s.camelize).new(value)
+ end
+
+ specifications.compact
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
new file mode 100644
index 00000000000..b20d374288f
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ class Kubernetes < Policy::Specification
+ def initialize(spec)
+ unless spec.to_sym == :active
+ raise UnknownPolicyError
+ end
+ end
+
+ def satisfied_by?(pipeline)
+ pipeline.has_kubernetes_active?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
new file mode 100644
index 00000000000..eadc0948d2f
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ class Refs < Policy::Specification
+ def initialize(refs)
+ @patterns = Array(refs)
+ end
+
+ def satisfied_by?(pipeline)
+ @patterns.any? do |pattern|
+ pattern, path = pattern.split('@', 2)
+
+ matches_path?(path, pipeline) &&
+ matches_pattern?(pattern, pipeline)
+ end
+ end
+
+ private
+
+ def matches_path?(path, pipeline)
+ return true unless path
+
+ pipeline.project_full_path == path
+ end
+
+ def matches_pattern?(pattern, pipeline)
+ return true if pipeline.tag? && pattern == 'tags'
+ return true if pipeline.branch? && pattern == 'branches'
+ return true if pipeline.source == pattern
+ return true if pipeline.source&.pluralize == pattern
+
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ pipeline.ref
+ else
+ pattern == pipeline.ref
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
new file mode 100644
index 00000000000..c317291f29d
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ ##
+ # Abstract class that defines an interface of job policy
+ # specification.
+ #
+ # Used for job's only/except policy configuration.
+ #
+ class Specification
+ UnknownPolicyError = Class.new(StandardError)
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
new file mode 100644
index 00000000000..7df7b542d91
--- /dev/null
+++ b/lib/gitlab/ci/charts.rb
@@ -0,0 +1,118 @@
+module Gitlab
+ module Ci
+ module Charts
+ module DailyInterval
+ def grouped_count(query)
+ query
+ .group("DATE(#{::Ci::Pipeline.table_name}.created_at)")
+ .count(:created_at)
+ .transform_keys { |date| date.strftime(@format) }
+ end
+
+ def interval_step
+ @interval_step ||= 1.day
+ end
+ end
+
+ module MonthlyInterval
+ def grouped_count(query)
+ if Gitlab::Database.postgresql?
+ query
+ .group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
+ .count(:created_at)
+ .transform_keys(&:squish)
+ else
+ query
+ .group("DATE_FORMAT(#{::Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
+ .count(:created_at)
+ end
+ end
+
+ def interval_step
+ @interval_step ||= 1.month
+ end
+ end
+
+ class Chart
+ attr_reader :labels, :total, :success, :project, :pipeline_times
+
+ def initialize(project)
+ @labels = []
+ @total = []
+ @success = []
+ @pipeline_times = []
+ @project = project
+
+ collect
+ end
+
+ def collect
+ query = project.pipelines
+ .where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
+
+ totals_count = grouped_count(query)
+ success_count = grouped_count(query.success)
+
+ current = @from
+ while current < @to
+ label = current.strftime(@format)
+
+ @labels << label
+ @total << (totals_count[label] || 0)
+ @success << (success_count[label] || 0)
+
+ current += interval_step
+ end
+ end
+ end
+
+ class YearChart < Chart
+ include MonthlyInterval
+
+ def initialize(*)
+ @to = Date.today.end_of_month
+ @from = @to.years_ago(1).beginning_of_month
+ @format = '%d %B %Y'
+
+ super
+ end
+ end
+
+ class MonthChart < Chart
+ include DailyInterval
+
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 30.days
+ @format = '%d %B'
+
+ super
+ end
+ end
+
+ class WeekChart < Chart
+ include DailyInterval
+
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 7.days
+ @format = '%d %B'
+
+ super
+ end
+ end
+
+ class PipelineTime < Chart
+ def collect
+ commits = project.pipelines.last(30)
+
+ commits.each do |commit|
+ @labels << commit.short_sha
+ duration = commit.duration || 0
+ @pipeline_times << (duration / 60)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
new file mode 100644
index 00000000000..0daddaa638c
--- /dev/null
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module Ci::MaskSecret
+ class << self
+ def mask!(value, token)
+ return value unless value.present? && token.present?
+
+ value.gsub!(token, 'x' * token.length)
+ value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb
new file mode 100644
index 00000000000..3994a50772b
--- /dev/null
+++ b/lib/gitlab/ci/model.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Model
+ def table_name_prefix
+ "ci_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
new file mode 100644
index 00000000000..8d82e1b288d
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Base
+ attr_reader :pipeline, :project, :current_user
+
+ def initialize(pipeline, command)
+ @pipeline = pipeline
+ @command = command
+
+ @project = command.project
+ @current_user = command.current_user
+ end
+
+ def perform!
+ raise NotImplementedError
+ end
+
+ def break?
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
new file mode 100644
index 00000000000..d5e17a123df
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Create < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ ::Ci::Pipeline.transaction do
+ pipeline.save!
+
+ @command.seeds_block&.call(pipeline)
+
+ ::Ci::CreatePipelineStagesService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ error("Failed to persist the pipeline: #{e}")
+ end
+
+ def break?
+ !pipeline.persisted?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
new file mode 100644
index 00000000000..02d81286f21
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Helpers
+ def branch_exists?
+ return @is_branch if defined?(@is_branch)
+
+ @is_branch = project.repository.branch_exists?(pipeline.ref)
+ end
+
+ def tag_exists?
+ return @is_tag if defined?(@is_tag)
+
+ @is_tag = project.repository.tag_exists?(pipeline.ref)
+ end
+
+ def error(message)
+ pipeline.errors.add(:base, message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
new file mode 100644
index 00000000000..015f2988327
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Sequence
+ def initialize(pipeline, command, sequence)
+ @pipeline = pipeline
+ @completed = []
+
+ @sequence = sequence.map do |chain|
+ chain.new(pipeline, command)
+ end
+ end
+
+ def build!
+ @sequence.each do |step|
+ step.perform!
+
+ break if step.break?
+
+ @completed << step
+ end
+
+ @pipeline.tap do
+ yield @pipeline, self if block_given?
+ end
+ end
+
+ def complete?
+ @completed.size == @sequence.size
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
new file mode 100644
index 00000000000..9a72de87bab
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Skip < Chain::Base
+ SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
+
+ def perform!
+ if skipped?
+ @pipeline.skip if @command.save_incompleted
+ end
+ end
+
+ def skipped?
+ !@command.ignore_skip_ci && commit_message_skips_ci?
+ end
+
+ def break?
+ skipped?
+ end
+
+ private
+
+ def commit_message_skips_ci?
+ return false unless @pipeline.git_commit_message
+
+ @skipped ||= !!(@pipeline.git_commit_message =~ SKIP_PATTERN)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
new file mode 100644
index 00000000000..4913a604079
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Validate
+ class Abilities < Chain::Base
+ include Gitlab::Allowable
+ include Chain::Helpers
+
+ def perform!
+ unless project.builds_enabled?
+ return error('Pipelines are disabled!')
+ end
+
+ unless allowed_to_trigger_pipeline?
+ if can?(current_user, :create_pipeline, project)
+ return error("Insufficient permissions for protected ref '#{pipeline.ref}'")
+ else
+ return error('Insufficient permissions to create a new pipeline')
+ end
+ end
+ end
+
+ def break?
+ @pipeline.errors.any?
+ end
+
+ def allowed_to_trigger_pipeline?
+ if current_user
+ allowed_to_create?
+ else # legacy triggers don't have a corresponding user
+ !project.protected_for?(@pipeline.ref)
+ end
+ end
+
+ def allowed_to_create?
+ return unless can?(current_user, :create_pipeline, project)
+
+ access = Gitlab::UserAccess.new(current_user, project: project)
+
+ if branch_exists?
+ access.can_update_branch?(@pipeline.ref)
+ elsif tag_exists?
+ access.can_create_tag?(@pipeline.ref)
+ else
+ true # Allow it for now and we'll reject when we check ref existence
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
new file mode 100644
index 00000000000..075504bcce5
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Validate
+ class Config < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ unless @pipeline.config_processor
+ unless @pipeline.ci_yaml_file
+ return error("Missing #{@pipeline.ci_yaml_file_path} file")
+ end
+
+ if @command.save_incompleted && @pipeline.has_yaml_errors?
+ @pipeline.drop!(:config_error)
+ end
+
+ return error(@pipeline.yaml_errors)
+ end
+
+ unless @pipeline.has_stage_seeds?
+ return error('No stages / jobs for this pipeline.')
+ end
+ end
+
+ def break?
+ @pipeline.errors.any? || @pipeline.persisted?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
new file mode 100644
index 00000000000..70a4cfdbdea
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Validate
+ class Repository < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ unless branch_exists? || tag_exists?
+ return error('Reference not found')
+ end
+
+ ## TODO, we check commit in the service, that is why
+ # there is no repository access here.
+ #
+ unless pipeline.sha
+ return error('Commit not found')
+ end
+ end
+
+ def break?
+ @pipeline.errors.any?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb
new file mode 100644
index 00000000000..469fc094cc8
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/duration.rb
@@ -0,0 +1,143 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ # # Introduction - total running time
+ #
+ # The problem this module is trying to solve is finding the total running
+ # time amongst all the jobs, excluding retries and pending (queue) time.
+ # We could reduce this problem down to finding the union of periods.
+ #
+ # So each job would be represented as a `Period`, which consists of
+ # `Period#first` as when the job started and `Period#last` as when the
+ # job was finished. A simple example here would be:
+ #
+ # * A (1, 3)
+ # * B (2, 4)
+ # * C (6, 7)
+ #
+ # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
+ # C begins from 6, and ends to 7. Visually it could be viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # AAAAAAA
+ # BBBBBBB
+ # CCCC
+ #
+ # The union of A, B, and C would be (1, 4) and (6, 7), therefore the
+ # total running time should be:
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # # The Algorithm
+ #
+ # The algorithm used here for union would be described as follow.
+ # First we make sure that all periods are sorted by `Period#first`.
+ # Then we try to merge periods by iterating through the first period
+ # to the last period. The goal would be merging all overlapped periods
+ # so that in the end all the periods are discrete. When all periods
+ # are discrete, we're free to just sum all the periods to get real
+ # running time.
+ #
+ # Here we begin from A, and compare it to B. We could find that
+ # before A ends, B already started. That is `B.first <= A.last`
+ # that is `2 <= 3` which means A and B are overlapping!
+ #
+ # When we found that two periods are overlapping, we would need to merge
+ # them into a new period and disregard the old periods. To make a new
+ # period, we take `A.first` as the new first because remember? we sorted
+ # them, so `A.first` must be smaller or equal to `B.first`. And we take
+ # `[A.last, B.last].max` as the new last because we want whoever ended
+ # later. This could be broken into two cases:
+ #
+ # 0 1 2 3 4
+ # AAAAAAA
+ # BBBBBBB
+ #
+ # Or:
+ #
+ # 0 1 2 3 4
+ # AAAAAAAAAA
+ # BBBB
+ #
+ # So that we need to take whoever ends later. Back to our example,
+ # after merging and discard A and B it could be visually viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # DDDDDDDDDD
+ # CCCC
+ #
+ # Now we could go on and compare the newly created D and the old C.
+ # We could figure out that D and C are not overlapping by checking
+ # `C.first <= D.last` is `false`. Therefore we need to keep both C
+ # and D. The example would end here because there are no more jobs.
+ #
+ # After having the union of all periods, we just need to sum the length
+ # of all periods to get total time.
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # That is 4 is the answer in the example.
+ module Duration
+ extend self
+
+ Period = Struct.new(:first, :last) do
+ def duration
+ last - first
+ end
+ end
+
+ def from_pipeline(pipeline)
+ status = %w[success failed running canceled]
+ builds = pipeline.builds.latest
+ .where(status: status).where.not(started_at: nil).order(:started_at)
+
+ from_builds(builds)
+ end
+
+ def from_builds(builds)
+ now = Time.now
+
+ periods = builds.map do |b|
+ Period.new(b.started_at, b.finished_at || now)
+ end
+
+ from_periods(periods)
+ end
+
+ # periods should be sorted by `first`
+ def from_periods(periods)
+ process_duration(process_periods(periods))
+ end
+
+ private
+
+ def process_periods(periods)
+ return periods if periods.empty?
+
+ periods.drop(1).inject([periods.first]) do |result, current|
+ previous = result.last
+
+ if overlap?(previous, current)
+ result[-1] = merge(previous, current)
+ result
+ else
+ result << current
+ end
+ end
+ end
+
+ def overlap?(previous, current)
+ current.first <= previous.last
+ end
+
+ def merge(previous, current)
+ Period.new(previous.first, [previous.last, current.last].max)
+ end
+
+ def process_duration(periods)
+ periods.sum(&:duration)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb
deleted file mode 100644
index 3208cc2bef6..00000000000
--- a/lib/gitlab/ci/pipeline_duration.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-module Gitlab
- module Ci
- # # Introduction - total running time
- #
- # The problem this module is trying to solve is finding the total running
- # time amongst all the jobs, excluding retries and pending (queue) time.
- # We could reduce this problem down to finding the union of periods.
- #
- # So each job would be represented as a `Period`, which consists of
- # `Period#first` as when the job started and `Period#last` as when the
- # job was finished. A simple example here would be:
- #
- # * A (1, 3)
- # * B (2, 4)
- # * C (6, 7)
- #
- # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
- # C begins from 6, and ends to 7. Visually it could be viewed as:
- #
- # 0 1 2 3 4 5 6 7
- # AAAAAAA
- # BBBBBBB
- # CCCC
- #
- # The union of A, B, and C would be (1, 4) and (6, 7), therefore the
- # total running time should be:
- #
- # (4 - 1) + (7 - 6) => 4
- #
- # # The Algorithm
- #
- # The algorithm used here for union would be described as follow.
- # First we make sure that all periods are sorted by `Period#first`.
- # Then we try to merge periods by iterating through the first period
- # to the last period. The goal would be merging all overlapped periods
- # so that in the end all the periods are discrete. When all periods
- # are discrete, we're free to just sum all the periods to get real
- # running time.
- #
- # Here we begin from A, and compare it to B. We could find that
- # before A ends, B already started. That is `B.first <= A.last`
- # that is `2 <= 3` which means A and B are overlapping!
- #
- # When we found that two periods are overlapping, we would need to merge
- # them into a new period and disregard the old periods. To make a new
- # period, we take `A.first` as the new first because remember? we sorted
- # them, so `A.first` must be smaller or equal to `B.first`. And we take
- # `[A.last, B.last].max` as the new last because we want whoever ended
- # later. This could be broken into two cases:
- #
- # 0 1 2 3 4
- # AAAAAAA
- # BBBBBBB
- #
- # Or:
- #
- # 0 1 2 3 4
- # AAAAAAAAAA
- # BBBB
- #
- # So that we need to take whoever ends later. Back to our example,
- # after merging and discard A and B it could be visually viewed as:
- #
- # 0 1 2 3 4 5 6 7
- # DDDDDDDDDD
- # CCCC
- #
- # Now we could go on and compare the newly created D and the old C.
- # We could figure out that D and C are not overlapping by checking
- # `C.first <= D.last` is `false`. Therefore we need to keep both C
- # and D. The example would end here because there are no more jobs.
- #
- # After having the union of all periods, we just need to sum the length
- # of all periods to get total time.
- #
- # (4 - 1) + (7 - 6) => 4
- #
- # That is 4 is the answer in the example.
- module PipelineDuration
- extend self
-
- Period = Struct.new(:first, :last) do
- def duration
- last - first
- end
- end
-
- def from_pipeline(pipeline)
- status = %w[success failed running canceled]
- builds = pipeline.builds.latest
- .where(status: status).where.not(started_at: nil).order(:started_at)
-
- from_builds(builds)
- end
-
- def from_builds(builds)
- now = Time.now
-
- periods = builds.map do |b|
- Period.new(b.started_at, b.finished_at || now)
- end
-
- from_periods(periods)
- end
-
- # periods should be sorted by `first`
- def from_periods(periods)
- process_duration(process_periods(periods))
- end
-
- private
-
- def process_periods(periods)
- return periods if periods.empty?
-
- periods.drop(1).inject([periods.first]) do |result, current|
- previous = result.last
-
- if overlap?(previous, current)
- result[-1] = merge(previous, current)
- result
- else
- result << current
- end
- end
- end
-
- def overlap?(previous, current)
- current.first <= previous.last
- end
-
- def merge(previous, current)
- Period.new(previous.first, [previous.last, current.last].max)
- end
-
- def process_duration(periods)
- periods.sum(&:duration)
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
index e19aae35a81..bc97aa63b02 100644
--- a/lib/gitlab/ci/stage/seed.rb
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -3,7 +3,9 @@ module Gitlab
module Stage
class Seed
attr_reader :pipeline
+
delegate :project, to: :pipeline
+ delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 5b835bb669a..baf55b1fa07 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -27,6 +27,12 @@ module Gitlab
end
end
+ def extract_sections
+ read do |stream|
+ stream.extract_sections
+ end
+ end
+
def set(data)
write do |stream|
data = job.hide_secrets(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
new file mode 100644
index 00000000000..9bb0166c9e3
--- /dev/null
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module Ci
+ class Trace
+ class SectionParser
+ def initialize(lines)
+ @lines = lines
+ end
+
+ def parse!
+ @markers = {}
+
+ @lines.each do |line, pos|
+ parse_line(line, pos)
+ end
+ end
+
+ def sections
+ sanitize_markers.map do |name, markers|
+ start_, end_ = markers
+
+ {
+ name: name,
+ byte_start: start_[:marker],
+ byte_end: end_[:marker],
+ date_start: start_[:timestamp],
+ date_end: end_[:timestamp]
+ }
+ end
+ end
+
+ private
+
+ def parse_line(line, line_start_position)
+ s = StringScanner.new(line)
+ until s.eos?
+ find_next_marker(s) do |scanner|
+ marker_begins_at = line_start_position + scanner.pointer
+
+ if scanner.scan(Gitlab::Regex.build_trace_section_regex)
+ marker_ends_at = line_start_position + scanner.pointer
+ handle_line(scanner[1], scanner[2].to_i, scanner[3], marker_begins_at, marker_ends_at)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+
+ def sanitize_markers
+ @markers.select do |_, markers|
+ markers.size == 2 && markers[0][:action] == :start && markers[1][:action] == :end
+ end
+ end
+
+ def handle_line(action, time, name, marker_start, marker_end)
+ action = action.to_sym
+ timestamp = Time.at(time).utc
+ marker = if action == :start
+ marker_end
+ else
+ marker_start
+ end
+
+ @markers[name] ||= []
+ @markers[name] << {
+ name: name,
+ action: action,
+ timestamp: timestamp,
+ marker: marker
+ }
+ end
+
+ def beginning_of_section_regex
+ @beginning_of_section_regex ||= /section_/.freeze
+ end
+
+ def find_next_marker(s)
+ beginning_of_section_len = 8
+ maybe_marker = s.exist?(beginning_of_section_regex)
+
+ if maybe_marker.nil?
+ s.terminate
+ else
+ # repositioning at the beginning of the match
+ s.pos += maybe_marker - beginning_of_section_len
+ if block_given?
+ good_marker = yield(s)
+ # if not a good marker: Consuming the matched beginning_of_section_regex
+ s.pos += beginning_of_section_len unless good_marker
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 8503ecf8700..d52194f688b 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -56,13 +56,13 @@ module Gitlab
end
def html_with_state(state = nil)
- ::Ci::Ansi2html.convert(stream, state)
+ ::Gitlab::Ci::Ansi2html.convert(stream, state)
end
def html(last_lines: nil)
text = raw(last_lines: last_lines)
buffer = StringIO.new(text)
- ::Ci::Ansi2html.convert(buffer).html
+ ::Gitlab::Ci::Ansi2html.convert(buffer).html
end
def extract_coverage(regex)
@@ -90,8 +90,25 @@ module Gitlab
# so we just silently ignore error for now
end
+ def extract_sections
+ return [] unless valid?
+
+ lines = to_enum(:each_line_with_pos)
+ parser = SectionParser.new(lines)
+
+ parser.parse!
+ parser.sections
+ end
+
private
+ def each_line_with_pos
+ stream.seek(0, IO::SEEK_SET)
+ stream.each_line do |line|
+ yield [line, stream.pos - line.bytesize]
+ end
+ end
+
def read_last_lines(limit)
to_enum(:reverse_line).first(limit).reverse.join
end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
new file mode 100644
index 00000000000..0bd78b03448
--- /dev/null
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -0,0 +1,189 @@
+module Gitlab
+ module Ci
+ class YamlProcessor
+ ValidationError = Class.new(StandardError)
+
+ include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
+
+ attr_reader :cache, :stages, :jobs
+
+ def initialize(config)
+ @ci_config = Gitlab::Ci::Config.new(config)
+ @config = @ci_config.to_hash
+
+ unless @ci_config.valid?
+ raise ValidationError, @ci_config.errors.first
+ end
+
+ initial_parsing
+ rescue Gitlab::Ci::Config::Loader::FormatError => e
+ raise ValidationError, e.message
+ end
+
+ def builds
+ @jobs.map do |name, _|
+ build_attributes(name)
+ end
+ end
+
+ def build_attributes(name)
+ job = @jobs[name.to_sym] || {}
+
+ { stage_idx: @stages.index(job[:stage]),
+ stage: job[:stage],
+ commands: job[:commands],
+ tag_list: job[:tags] || [],
+ name: job[:name].to_s,
+ allow_failure: job[:ignore],
+ when: job[:when] || 'on_success',
+ environment: job[:environment_name],
+ coverage_regex: job[:coverage],
+ yaml_variables: yaml_variables(name),
+ options: {
+ image: job[:image],
+ services: job[:services],
+ artifacts: job[:artifacts],
+ cache: job[:cache],
+ dependencies: job[:dependencies],
+ before_script: job[:before_script],
+ script: job[:script],
+ after_script: job[:after_script],
+ environment: job[:environment],
+ retry: job[:retry]
+ }.compact }
+ end
+
+ def pipeline_stage_builds(stage, pipeline)
+ selected_jobs = @jobs.select do |_, job|
+ next unless job[:stage] == stage
+
+ only_specs = Gitlab::Ci::Build::Policy
+ .fabricate(job.fetch(:only, {}))
+ except_specs = Gitlab::Ci::Build::Policy
+ .fabricate(job.fetch(:except, {}))
+
+ only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
+ except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
+ end
+
+ selected_jobs.map { |_, job| build_attributes(job[:name]) }
+ end
+
+ def stage_seeds(pipeline)
+ seeds = @stages.uniq.map do |stage|
+ builds = pipeline_stage_builds(stage, pipeline)
+
+ Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
+ end
+
+ seeds.compact
+ end
+
+ def self.validation_message(content)
+ return 'Please provide content of .gitlab-ci.yml' if content.blank?
+
+ begin
+ Gitlab::Ci::YamlProcessor.new(content)
+ nil
+ rescue ValidationError, Psych::SyntaxError => e
+ e.message
+ end
+ end
+
+ private
+
+ def initial_parsing
+ ##
+ # Global config
+ #
+ @before_script = @ci_config.before_script
+ @image = @ci_config.image
+ @after_script = @ci_config.after_script
+ @services = @ci_config.services
+ @variables = @ci_config.variables
+ @stages = @ci_config.stages
+ @cache = @ci_config.cache
+
+ ##
+ # Jobs
+ #
+ @jobs = @ci_config.jobs
+
+ @jobs.each do |name, job|
+ # logical validation for job
+
+ validate_job_stage!(name, job)
+ validate_job_dependencies!(name, job)
+ validate_job_environment!(name, job)
+ end
+ end
+
+ def yaml_variables(name)
+ variables = (@variables || {})
+ .merge(job_variables(name))
+
+ variables.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
+ end
+
+ def job_variables(name)
+ job = @jobs[name.to_sym]
+ return {} unless job
+
+ job[:variables] || {}
+ end
+
+ def validate_job_stage!(name, job)
+ return unless job[:stage]
+
+ unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
+ raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
+ end
+ end
+
+ def validate_job_dependencies!(name, job)
+ return unless job[:dependencies]
+
+ stage_index = @stages.index(job[:stage])
+
+ job[:dependencies].each do |dependency|
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
+
+ unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
+ end
+ end
+ end
+
+ def validate_job_environment!(name, job)
+ return unless job[:environment]
+ return unless job[:environment].is_a?(Hash)
+
+ environment = job[:environment]
+ validate_on_stop_job!(name, environment, environment[:on_stop])
+ end
+
+ def validate_on_stop_job!(name, environment, on_stop)
+ return unless on_stop
+
+ on_stop_job = @jobs[on_stop.to_sym]
+ unless on_stop_job
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
+ end
+
+ unless on_stop_job[:environment]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
+ end
+
+ unless on_stop_job[:environment][:name] == environment[:name]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
+ end
+
+ unless on_stop_job[:environment][:action] == 'stop'
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 58f86abc5c4..7e7aaeeaa17 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -1,7 +1,7 @@
module Gitlab
class ClosingIssueExtractor
ISSUE_CLOSING_REGEX = begin
- link_pattern = URI.regexp(%w(http https))
+ link_pattern = Banzai::Filter::AutolinkFilter::LINK_PATTERN
pattern = Gitlab.config.gitlab.issue_closing_pattern
pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))")
@@ -23,7 +23,8 @@ module Gitlab
@extractor.analyze(closing_statements.join(" "))
@extractor.issues.reject do |issue|
- @extractor.project.forked_from?(issue.project) # Don't extract issues on original project
+ # Don't extract issues from the project this project was forked from
+ @extractor.project.forked_from?(issue.project)
end
end
end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 84f9ecd3d23..e3678c914db 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -12,12 +12,7 @@ module Gitlab
MissingEndDelimiter = Class.new(ParserError)
def parse(text, our_path:, their_path:, parent_file: nil)
- raise UnmergeableFile if text.blank? # Typically a binary file
- raise UnmergeableFile if text.length > 200.kilobytes
-
- text.force_encoding('UTF-8')
-
- raise UnsupportedEncoding unless text.valid_encoding?
+ validate_text!(text)
line_obj_index = 0
line_old = 1
@@ -32,15 +27,15 @@ module Gitlab
full_line = line.delete("\n")
if full_line == conflict_start
- raise UnexpectedDelimiter unless type.nil?
+ validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
- raise UnexpectedDelimiter unless type == 'new'
+ validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
- raise UnexpectedDelimiter unless type == 'old'
+ validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
@@ -59,6 +54,21 @@ module Gitlab
lines
end
+
+ private
+
+ def validate_text!(text)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 200.kilobytes
+
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
+ end
+
+ def validate_delimiter!(condition)
+ raise UnexpectedDelimiter unless condition
+ end
end
end
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 4ab5b3455a5..c169c8fe135 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -64,8 +64,11 @@ module Gitlab
# For performance purposes maximum 20 latest commits
# will be passed as post receive hook data.
- commit_attrs = commits_limited.map do |commit|
- commit.hook_attrs(with_changed_files: true)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259
+ commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits_limited.map do |commit|
+ commit.hook_attrs(with_changed_files: true)
+ end
end
type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
@@ -83,7 +86,7 @@ module Gitlab
user_name: user.name,
user_username: user.username,
user_email: user.email,
- user_avatar: user.avatar_url,
+ user_avatar: user.avatar_url(only_path: false),
project_id: project.id,
project: project.hook_attrs,
commits: commit_attrs,
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a6ec75da385..357f16936c6 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -29,6 +29,15 @@ module Gitlab
adapter_name.casecmp('postgresql').zero?
end
+ # Overridden in EE
+ def self.read_only?
+ false
+ end
+
+ def self.read_write?
+ !self.read_only?
+ end
+
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index fb14798efe6..2c35da8f1aa 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,6 +1,9 @@
module Gitlab
module Database
module MigrationHelpers
+ BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
+ BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
+
# Adds `created_at` and `updated_at` columns with timezone information.
#
# This method is an improved version of Rails' built-in method `add_timestamps`.
@@ -653,6 +656,91 @@ into similar problems in the future (e.g. when new tables are created).
EOF
end
end
+
+ # Bulk queues background migration jobs for an entire table, batched by ID range.
+ # "Bulk" meaning many jobs will be pushed at a time for efficiency.
+ # If you need a delay interval per job, then use `queue_background_migration_jobs_by_range_at_intervals`.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # bulk_queue_background_migration_jobs_by_range(Route, 'ProcessRoutes')
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ jobs = []
+
+ model_class.each_batch(of: batch_size) do |relation|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
+ # Note: This code path generally only helps with many millions of rows
+ # We push multiple jobs at a time to reduce the time spent in
+ # Sidekiq/Redis operations. We're using this buffer based approach so we
+ # don't need to run additional queries for every range.
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ jobs.clear
+ end
+
+ jobs << [job_class_name, [start_id, end_id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty?
+ end
+
+ # Queues background migration jobs for an entire table, batched by ID range.
+ # Each job is scheduled with a `delay_interval` in between.
+ # If you use a small interval, then some jobs may run at the same time.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # queue_background_migration_jobs_by_range_at_intervals(Route, 'ProcessRoutes', 1.minute)
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ model_class.each_batch(of: batch_size) do |relation, index|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
+ # the same time, which is not helpful in most cases where we wish to
+ # spread the work over time.
+ BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb
new file mode 100644
index 00000000000..4571ad122ce
--- /dev/null
+++ b/lib/gitlab/database/read_only_relation.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Database
+ # Module that can be injected into a ActiveRecord::Relation to make it
+ # read-only.
+ module ReadOnlyRelation
+ [:delete, :delete_all, :update, :update_all].each do |method|
+ define_method(method) do |*args|
+ raise(
+ ActiveRecord::ReadOnlyRecord,
+ "This relation is marked as read-only"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
index 371cbe04b9b..c98eefbce25 100644
--- a/lib/gitlab/diff/diff_refs.rb
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -13,9 +13,9 @@ module Gitlab
def ==(other)
other.is_a?(self.class) &&
- base_sha == other.base_sha &&
- start_sha == other.start_sha &&
- head_sha == other.head_sha
+ shas_equal?(base_sha, other.base_sha) &&
+ shas_equal?(start_sha, other.start_sha) &&
+ shas_equal?(head_sha, other.head_sha)
end
alias_method :eql?, :==
@@ -47,6 +47,22 @@ module Gitlab
CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
end
end
+
+ private
+
+ def shas_equal?(sha1, sha2)
+ return true if sha1 == sha2
+ return false if sha1.nil? || sha2.nil?
+ return false unless sha1.class == sha2.class
+
+ length = [sha1.length, sha2.length].min
+
+ # If either of the shas is below the minimum length, we cannot be sure
+ # that they actually refer to the same commit because of hash collision.
+ return false if length < Commit::MIN_SHA_LENGTH
+
+ sha1[0, length] == sha2[0, length]
+ end
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 1dabd4ebdd0..599c3c5deab 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -5,7 +5,7 @@ module Gitlab
delegate :new_file?, :deleted_file?, :renamed_file?,
:old_path, :new_path, :a_mode, :b_mode, :mode_changed?,
- :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, to: :diff, prefix: false
+ :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, :has_binary_notice?, to: :diff, prefix: false
# Finding a viewer for a diff file happens based only on extension and whether the
# diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer,
@@ -27,16 +27,23 @@ module Gitlab
@fallback_diff_refs = fallback_diff_refs
end
- def position(line)
+ def position(position_marker, position_type: :text)
return unless diff_refs
- Position.new(
+ data = {
+ diff_refs: diff_refs,
+ position_type: position_type.to_s,
old_path: old_path,
- new_path: new_path,
- old_line: line.old_line,
- new_line: line.new_line,
- diff_refs: diff_refs
- )
+ new_path: new_path
+ }
+
+ if position_type == :text
+ data.merge!(text_position_properties(position_marker))
+ else
+ data.merge!(image_position_properties(position_marker))
+ end
+
+ Position.new(data)
end
def line_code(line)
@@ -166,7 +173,7 @@ module Gitlab
end
def binary?
- old_blob&.binary? || new_blob&.binary?
+ has_binary_notice? || old_blob&.binary? || new_blob&.binary?
end
def text?
@@ -228,6 +235,14 @@ module Gitlab
private
+ def text_position_properties(line)
+ { old_line: line.old_line, new_line: line.new_line }
+ end
+
+ def image_position_properties(image_point)
+ image_point.to_h
+ end
+
def blobs_changed?
old_blob && new_blob && old_blob.id != new_blob.id
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index a6007ebf531..88ae65cb468 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -22,7 +22,10 @@ module Gitlab
end
def diff_files
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ end
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/diff/formatters/base_formatter.rb b/lib/gitlab/diff/formatters/base_formatter.rb
new file mode 100644
index 00000000000..5e923b9e602
--- /dev/null
+++ b/lib/gitlab/diff/formatters/base_formatter.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class BaseFormatter
+ attr_reader :old_path
+ attr_reader :new_path
+ attr_reader :base_sha
+ attr_reader :start_sha
+ attr_reader :head_sha
+ attr_reader :position_type
+
+ def initialize(attrs)
+ if diff_file = attrs[:diff_file]
+ attrs[:diff_refs] = diff_file.diff_refs
+ attrs[:old_path] = diff_file.old_path
+ attrs[:new_path] = diff_file.new_path
+ end
+
+ if diff_refs = attrs[:diff_refs]
+ attrs[:base_sha] = diff_refs.base_sha
+ attrs[:start_sha] = diff_refs.start_sha
+ attrs[:head_sha] = diff_refs.head_sha
+ end
+
+ @old_path = attrs[:old_path]
+ @new_path = attrs[:new_path]
+ @base_sha = attrs[:base_sha]
+ @start_sha = attrs[:start_sha]
+ @head_sha = attrs[:head_sha]
+ end
+
+ def key
+ [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || "")]
+ end
+
+ def to_h
+ {
+ base_sha: base_sha,
+ start_sha: start_sha,
+ head_sha: head_sha,
+ old_path: old_path,
+ new_path: new_path,
+ position_type: position_type
+ }
+ end
+
+ def position_type
+ raise NotImplementedError
+ end
+
+ def ==(other)
+ raise NotImplementedError
+ end
+
+ def complete?
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
new file mode 100644
index 00000000000..ccd0d309972
--- /dev/null
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class ImageFormatter < BaseFormatter
+ attr_reader :width
+ attr_reader :height
+ attr_reader :x
+ attr_reader :y
+
+ def initialize(attrs)
+ @x = attrs[:x]
+ @y = attrs[:y]
+ @width = attrs[:width]
+ @height = attrs[:height]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(x, y)
+ end
+
+ def complete?
+ x && y && width && height
+ end
+
+ def to_h
+ super.merge(width: width, height: height, x: x, y: y)
+ end
+
+ def position_type
+ "image"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ x == other.x &&
+ y == other.y
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
new file mode 100644
index 00000000000..01c7e9f51ab
--- /dev/null
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class TextFormatter < BaseFormatter
+ attr_reader :old_line
+ attr_reader :new_line
+
+ def initialize(attrs)
+ @old_line = attrs[:old_line]
+ @new_line = attrs[:new_line]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(old_line, new_line)
+ end
+
+ def complete?
+ old_line || new_line
+ end
+
+ def to_h
+ super.merge(old_line: old_line, new_line: new_line)
+ end
+
+ def line_age
+ if old_line && new_line
+ nil
+ elsif new_line
+ 'new'
+ else
+ 'old'
+ end
+ end
+
+ def position_type
+ "text"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ new_line == other.new_line &&
+ old_line == other.old_line
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
new file mode 100644
index 00000000000..65332dfd239
--- /dev/null
+++ b/lib/gitlab/diff/image_point.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Diff
+ class ImagePoint
+ attr_reader :width, :height, :x, :y
+
+ def initialize(width, height, x, y)
+ @width = width
+ @height = height
+ @x = x
+ @y = y
+ end
+
+ def to_h
+ {
+ width: width,
+ height: height,
+ x: x,
+ y: y
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 919965100ae..010b4be7b40 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -2,9 +2,10 @@ module Gitlab
module Diff
class InlineDiffMarker < Gitlab::StringRangeMarker
def mark(line_inline_diffs, mode: nil)
- super(line_inline_diffs) do |text, left:, right:|
+ mark = super(line_inline_diffs) do |text, left:, right:|
%{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}
end
+ mark.html_safe
end
private
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index f80afb20f0c..bd0a9502a5e 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -1,37 +1,25 @@
-# Defines a specific location, identified by paths and line numbers,
+# Defines a specific location, identified by paths line numbers and image coordinates,
# within a specific diff, identified by start, head and base commit ids.
module Gitlab
module Diff
class Position
- attr_reader :old_path
- attr_reader :new_path
- attr_reader :old_line
- attr_reader :new_line
- attr_reader :base_sha
- attr_reader :start_sha
- attr_reader :head_sha
-
+ attr_accessor :formatter
+
+ delegate :old_path,
+ :new_path,
+ :base_sha,
+ :start_sha,
+ :head_sha,
+ :old_line,
+ :new_line,
+ :position_type, to: :formatter
+
+ # A position can belong to a text line or to an image coordinate
+ # it depends of the position_type argument.
+ # Text position will have: new_line and old_line
+ # Image position will have: width, height, x, y
def initialize(attrs = {})
- if diff_file = attrs[:diff_file]
- attrs[:diff_refs] = diff_file.diff_refs
- attrs[:old_path] = diff_file.old_path
- attrs[:new_path] = diff_file.new_path
- end
-
- if diff_refs = attrs[:diff_refs]
- attrs[:base_sha] = diff_refs.base_sha
- attrs[:start_sha] = diff_refs.start_sha
- attrs[:head_sha] = diff_refs.head_sha
- end
-
- @old_path = attrs[:old_path]
- @new_path = attrs[:new_path]
- @base_sha = attrs[:base_sha]
- @start_sha = attrs[:start_sha]
- @head_sha = attrs[:head_sha]
-
- @old_line = attrs[:old_line]
- @new_line = attrs[:new_line]
+ @formatter = get_formatter_class(attrs[:position_type]).new(attrs)
end
# `Gitlab::Diff::Position` objects are stored as serialized attributes in
@@ -46,27 +34,23 @@ module Gitlab
end
def encode_with(coder)
- coder['attributes'] = self.to_h
+ coder['attributes'] = formatter.to_h
end
def key
- @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line]
+ formatter.key
end
def ==(other)
- other.is_a?(self.class) && key == other.key
+ other.is_a?(self.class) &&
+ other.diff_refs == diff_refs &&
+ other.old_path == old_path &&
+ other.new_path == new_path &&
+ other.formatter == formatter
end
def to_h
- {
- old_path: old_path,
- new_path: new_path,
- old_line: old_line,
- new_line: new_line,
- base_sha: base_sha,
- start_sha: start_sha,
- head_sha: head_sha
- }
+ formatter.to_h
end
def inspect
@@ -74,23 +58,15 @@ module Gitlab
end
def complete?
- file_path.present? &&
- (old_line || new_line) &&
- diff_refs.complete?
+ file_path.present? && formatter.complete? && diff_refs.complete?
end
def to_json(opts = nil)
- JSON.generate(self.to_h, opts)
+ JSON.generate(formatter.to_h, opts)
end
def type
- if old_line && new_line
- nil
- elsif new_line
- 'new'
- else
- 'old'
- end
+ formatter.line_age
end
def unchanged?
@@ -149,6 +125,17 @@ module Gitlab
diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
end
+
+ def get_formatter_class(type)
+ type ||= "text"
+
+ case type
+ when 'image'
+ Gitlab::Diff::Formatters::ImageFormatter
+ else
+ Gitlab::Diff::Formatters::TextFormatter
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index abd401224d8..c4c60d1dfee 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,7 +2,7 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
+ DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
@@ -20,7 +20,7 @@ module Gitlab
attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
attr_reader :failed_files
- def initialize(branch:, ce_repo: CE_REPO)
+ def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@@ -132,7 +132,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
@@ -284,13 +284,18 @@ module Gitlab
EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
was found in the EE repository.
+ If you're a community contributor, don't worry, someone from
+ GitLab Inc. will take care of this, and you don't have to do anything.
+ If you're willing to help, and are ok to contribute to EE as well,
+ you're welcome to help. You could follow the instructions below.
+
#{conflicting_files_msg}
We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}`
branch that includes changes from `#{ce_branch}` but also specific changes
than can be applied cleanly to EE/master. In some cases, the conflicts
are trivial and you can ignore the warning from this job. As always,
- use your best judgment!
+ use your best judgement!
There are different ways to create such branch:
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
new file mode 100644
index 00000000000..195391f0e3c
--- /dev/null
+++ b/lib/gitlab/gcp/model.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Gcp
+ module Model
+ def table_name_prefix
+ "gcp_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index b984492d369..455814a9159 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -29,6 +29,8 @@ module Gitlab
# http://gitlab.com/some/link/#1234, and code `puts #1234`'
#
class ReferenceRewriter
+ RewriteError = Class.new(StandardError)
+
def initialize(text, source_project, current_user)
@text = text
@source_project = source_project
@@ -61,6 +63,10 @@ module Gitlab
cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
+ if cross_reference.nil?
+ raise RewriteError, "Unspecified reference detected for #{referable.class.name}"
+ end
+
new_text = before + cross_reference + after
substitution_valid?(new_text) ? cross_reference : reference
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 8c9acbc9fbe..c78fe63f9b5 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -11,7 +11,7 @@ module Gitlab
include Gitlab::EncodingHelper
def ref_name(ref)
- encode! ref.sub(/\Arefs\/(tags|heads|remotes)\//, '')
+ encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
end
def branch_name(ref)
@@ -57,6 +57,15 @@ module Gitlab
def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
end
+
+ def check_namespace!(*objects)
+ expected_namespace = self.name + '::'
+ objects.each do |object|
+ unless object.class.name.start_with?(expected_namespace)
+ raise ArgumentError, "expected object in #{expected_namespace}, got #{object}"
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 8d96826f6ee..a4336facee5 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -32,6 +32,8 @@ module Gitlab
else
blob = repository.lookup(sha)
+ next unless blob.is_a?(Rugged::Blob)
+
new(
id: blob.oid,
size: blob.size,
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 5ee6669050c..1957c254c28 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -352,7 +352,7 @@ module Gitlab
end
def stats
- Gitlab::Git::CommitStats.new(self)
+ Gitlab::Git::CommitStats.new(@repository, self)
end
def to_patch(options = {})
@@ -413,6 +413,10 @@ module Gitlab
end
end
+ def merge_commit?
+ parent_ids.size > 1
+ end
+
private
def init_from_hash(hash)
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 00acb4763e9..6bf49a0af18 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -10,12 +10,29 @@ module Gitlab
# Instantiate a CommitStats object
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323
- def initialize(commit)
+ def initialize(repo, commit)
@id = commit.id
@additions = 0
@deletions = 0
@total = 0
+ repo.gitaly_migrate(:commit_stats) do |is_enabled|
+ if is_enabled
+ gitaly_stats(repo, commit)
+ else
+ rugged_stats(commit)
+ end
+ end
+ end
+
+ def gitaly_stats(repo, commit)
+ stats = repo.gitaly_commit_client.commit_stats(@id)
+ @additions = stats.additions
+ @deletions = stats.deletions
+ @total = @additions + @deletions
+ end
+
+ def rugged_stats(commit)
diff = commit.rugged_diff_from_parent
diff.each_patch do |p|
diff --git a/lib/gitlab/git/committer.rb b/lib/gitlab/git/committer.rb
deleted file mode 100644
index 1f4bcf7a3a0..00000000000
--- a/lib/gitlab/git/committer.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module Gitlab
- module Git
- class Committer
- attr_reader :name, :email, :gl_id
-
- def self.from_user(user)
- new(user.name, user.email, Gitlab::GlId.gl_id(user))
- end
-
- def initialize(name, email, gl_id)
- @name = name
- @email = email
- @gl_id = gl_id
- end
-
- def ==(other)
- [name, email, gl_id] == [other.name, other.email, other.gl_id]
- end
- end
- end
-end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index a23c8cf0dd1..ca94b4baa59 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -24,41 +24,13 @@ module Gitlab
SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze
- class << self
- # The maximum size of a diff to display.
- def size_limit
- if RequestStore.active?
- RequestStore['gitlab_git_diff_size_limit'] ||= find_size_limit
- else
- find_size_limit
- end
- end
-
- # The maximum size before a diff is collapsed.
- def collapse_limit
- if RequestStore.active?
- RequestStore['gitlab_git_diff_collapse_limit'] ||= find_collapse_limit
- else
- find_collapse_limit
- end
- end
-
- def find_size_limit
- if Feature.enabled?('gitlab_git_diff_size_limit_increase')
- 200.kilobytes
- else
- 100.kilobytes
- end
- end
+ # The maximum size of a diff to display.
+ SIZE_LIMIT = 100.kilobytes
- def find_collapse_limit
- if Feature.enabled?('gitlab_git_diff_size_limit_increase')
- 100.kilobytes
- else
- 10.kilobytes
- end
- end
+ # The maximum size before a diff is collapsed.
+ COLLAPSE_LIMIT = 10.kilobytes
+ class << self
def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false
@@ -172,7 +144,7 @@ module Gitlab
def too_large?
if @too_large.nil?
- @too_large = @diff.bytesize >= self.class.size_limit
+ @too_large = @diff.bytesize >= SIZE_LIMIT
else
@too_large
end
@@ -190,7 +162,7 @@ module Gitlab
def collapsed?
return @collapsed if defined?(@collapsed)
- @collapsed = !expanded && @diff.bytesize >= self.class.collapse_limit
+ @collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT
end
def collapse!
@@ -206,6 +178,10 @@ module Gitlab
Diff.binary_message(@old_path, @new_path)
end
+ def has_binary_notice?
+ @diff.start_with?('Binary')
+ end
+
private
def init_from_rugged(rugged)
@@ -271,14 +247,14 @@ module Gitlab
hunk.each_line do |line|
size += line.content.bytesize
- if size >= self.class.size_limit
+ if size >= SIZE_LIMIT
too_large!
return true
end
end
end
- if !expanded && size >= self.class.collapse_limit
+ if !expanded && size >= COLLAPSE_LIMIT
collapse!
return true
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index cc35d77c6e4..e29a1f7afa1 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -22,22 +22,22 @@ module Gitlab
File.exist?(path)
end
- def trigger(gl_id, oldrev, newrev, ref)
+ def trigger(gl_id, gl_username, oldrev, newrev, ref)
return [true, nil] unless exists?
Bundler.with_clean_env do
case name
when "pre-receive", "post-receive"
- call_receive_hook(gl_id, oldrev, newrev, ref)
+ call_receive_hook(gl_id, gl_username, oldrev, newrev, ref)
when "update"
- call_update_hook(gl_id, oldrev, newrev, ref)
+ call_update_hook(gl_id, gl_username, oldrev, newrev, ref)
end
end
end
private
- def call_receive_hook(gl_id, oldrev, newrev, ref)
+ def call_receive_hook(gl_id, gl_username, oldrev, newrev, ref)
changes = [oldrev, newrev, ref].join(" ")
exit_status = false
@@ -45,6 +45,7 @@ module Gitlab
vars = {
'GL_ID' => gl_id,
+ 'GL_USERNAME' => gl_username,
'PWD' => repo_path,
'GL_PROTOCOL' => GL_PROTOCOL,
'GL_REPOSITORY' => repository.gl_repository
@@ -80,16 +81,21 @@ module Gitlab
[exit_status, exit_message]
end
- def call_update_hook(gl_id, oldrev, newrev, ref)
+ def call_update_hook(gl_id, gl_username, oldrev, newrev, ref)
Dir.chdir(repo_path) do
- stdout, stderr, status = Open3.capture3({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev)
- [status.success?, stderr.presence || stdout]
+ env = {
+ 'GL_ID' => gl_id,
+ 'GL_USERNAME' => gl_username
+ }
+ stdout, stderr, status = Open3.capture3(env, path, ref, oldrev, newrev)
+ [status.success?, (stderr.presence || stdout).gsub(/\R/, "<br>").html_safe]
end
end
def retrieve_error_message(stderr, stdout)
- err_message = stderr.gets
- err_message.blank? ? stdout.gets : err_message
+ err_message = stderr.read
+ err_message = err_message.blank? ? stdout.read : err_message
+ err_message.gsub(/\R/, "<br>").html_safe
end
end
end
diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb
index ea8a87a1290..c327e9b1616 100644
--- a/lib/gitlab/git/hooks_service.rb
+++ b/lib/gitlab/git/hooks_service.rb
@@ -5,12 +5,13 @@ module Gitlab
attr_accessor :oldrev, :newrev, :ref
- def execute(committer, repository, oldrev, newrev, ref)
- @repository = repository
- @gl_id = committer.gl_id
- @oldrev = oldrev
- @newrev = newrev
- @ref = ref
+ def execute(pusher, repository, oldrev, newrev, ref)
+ @repository = repository
+ @gl_id = pusher.gl_id
+ @gl_username = pusher.name
+ @oldrev = oldrev
+ @newrev = newrev
+ @ref = ref
%w(pre-receive update).each do |hook_name|
status, message = run_hook(hook_name)
@@ -29,7 +30,7 @@ module Gitlab
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repository)
- hook.trigger(@gl_id, oldrev, newrev, ref)
+ hook.trigger(@gl_id, @gl_username, oldrev, newrev, ref)
end
end
end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index 9e6fca8c80c..d835dcca8ba 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -1,17 +1,24 @@
module Gitlab
module Git
class OperationService
- attr_reader :committer, :repository
+ include Gitlab::Git::Popen
- def initialize(committer, new_repository)
- committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
- @committer = committer
+ WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
+ alias_method :repo_created?, :repo_created
+ alias_method :branch_created?, :branch_created
+ end
- # Refactoring aid
- unless new_repository.is_a?(Gitlab::Git::Repository)
- raise "expected a Gitlab::Git::Repository, got #{new_repository}"
+ attr_reader :user, :repository
+
+ def initialize(user, new_repository)
+ if user
+ user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
+ @user = user
end
+ # Refactoring aid
+ Gitlab::Git.check_namespace!(new_repository)
+
@repository = new_repository
end
@@ -105,7 +112,7 @@ module Gitlab
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
- [newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)]
+ WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
end
def find_oldrev_from_branch(newrev, branch)
@@ -128,7 +135,7 @@ module Gitlab
def with_hooks(ref, newrev, oldrev)
Gitlab::Git::HooksService.new.execute(
- committer,
+ user,
repository,
oldrev,
newrev,
@@ -145,13 +152,15 @@ module Gitlab
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
- _, status = Gitlab::Popen.popen(
+
+ output, status = popen(
command,
repository.path) do |stdin|
stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
end
unless status.zero?
+ Gitlab::GitLogger.error("'git update-ref' in #{repository.path}: #{output}")
raise Gitlab::Git::CommitError.new(
"Could not update branch #{Gitlab::Git.branch_name(ref)}." \
" Please refresh and try again.")
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index 25fa62ce4bd..3d2fc471d28 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -5,17 +5,21 @@ require 'open3'
module Gitlab
module Git
module Popen
- def popen(cmd, path)
+ def popen(cmd, path, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
- vars = { "PWD" => path }
+ path ||= Dir.pwd
+ vars['PWD'] = path
options = { chdir: path }
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ yield(stdin) if block_given?
+ stdin.close
+
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index efa13590a2c..0f059bef808 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -19,13 +19,12 @@ module Gitlab
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
+ CreateTreeError = Class.new(StandardError)
+ TagExistsError = Class.new(StandardError)
class << self
- # Unlike `new`, `create` takes the storage path, not the storage name
- def create(storage_path, name, bare: true, symlink_hooks_to: nil)
- repo_path = File.join(storage_path, name)
- repo_path += '.git' unless repo_path.end_with?('.git')
-
+ # Unlike `new`, `create` takes the repository path
+ def create(repo_path, bare: true, symlink_hooks_to: nil)
FileUtils.mkdir_p(repo_path, mode: 0770)
# Equivalent to `git --git-path=#{repo_path} init [--bare]`
@@ -54,14 +53,15 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
- attr_reader :storage, :gl_repository, :relative_path
+ attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver
- # 'path' must be the path to a _bare_ git repository, e.g.
- # /path/to/my-repo.git
+ # This initializer method is only used on the client side (gitlab-ce).
+ # Gitaly-ruby uses a different initializer.
def initialize(storage, relative_path, gl_repository)
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
+ @gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@@ -72,8 +72,6 @@ module Gitlab
delegate :empty?,
to: :rugged
- delegate :exists?, to: :gitaly_repository_client
-
def ==(other)
path == other.path
end
@@ -101,6 +99,18 @@ module Gitlab
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
end
+ def exists?
+ Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ if enabled
+ gitaly_repository_client.exists?
+ else
+ circuit_breaker.perform do
+ File.exist?(File.join(@path, 'refs'))
+ end
+ end
+ end
+ end
+
# Returns an Array of branch names
# sorted by name ASC
def branch_names
@@ -180,6 +190,28 @@ module Gitlab
end
end
+ def has_local_branches?
+ gitaly_migrate(:has_local_branches) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.has_local_branches?
+ else
+ has_local_branches_rugged?
+ end
+ end
+ end
+
+ def has_local_branches_rugged?
+ rugged.branches.each(:local).any? do |ref|
+ begin
+ ref.name && ref.target # ensures the branch is valid
+
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
+ end
+ end
+
# Returns the number of valid tags
def tag_count
gitaly_migrate(:tag_names) do |is_enabled|
@@ -385,7 +417,13 @@ module Gitlab
options[:limit] ||= 0
options[:offset] ||= 0
- raw_log(options).map { |c| Commit.decorate(self, c) }
+ gitaly_migrate(:find_commits) do |is_enabled|
+ if is_enabled
+ gitaly_commit_client.find_commits(options)
+ else
+ raw_log(options).map { |c| Commit.decorate(self, c) }
+ end
+ end
end
# Used in gitaly-ruby
@@ -474,7 +512,15 @@ module Gitlab
# diff options. The +options+ hash can also include :break_rewrites to
# split larger rewrites into delete/add pairs.
def diff(from, to, options = {}, *paths)
- Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options)
+ iterator = gitaly_migrate(:diff_between) do |is_enabled|
+ if is_enabled
+ gitaly_commit_client.diff(from, to, options.merge(paths: paths))
+ else
+ diff_patches(from, to, options, *paths)
+ end
+ end
+
+ Gitlab::Git::DiffCollection.new(iterator, options)
end
# Returns a RefName for a given SHA
@@ -489,7 +535,7 @@ module Gitlab
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- Gitlab::Popen.popen(args, @path).first.split.last
+ popen(args, @path).first.split.last
end
end
end
@@ -610,47 +656,167 @@ module Gitlab
# TODO: implement this method
end
- def add_branch(branch_name, committer:, target:)
- target_object = Ref.dereference_object(lookup(target))
- raise InvalidRef.new("target not found: #{target}") unless target_object
+ def add_branch(branch_name, user:, target:)
+ gitaly_migrate(:operation_user_create_branch) do |is_enabled|
+ if is_enabled
+ gitaly_add_branch(branch_name, user, target)
+ else
+ rugged_add_branch(branch_name, user, target)
+ end
+ end
+ end
- OperationService.new(committer, self).add_branch(branch_name, target_object.oid)
- find_branch(branch_name)
- rescue Rugged::ReferenceError => ex
- raise InvalidRef, ex
+ def add_tag(tag_name, user:, target:, message: nil)
+ gitaly_migrate(:operation_user_add_tag) do |is_enabled|
+ if is_enabled
+ gitaly_add_tag(tag_name, user: user, target: target, message: message)
+ else
+ rugged_add_tag(tag_name, user: user, target: target, message: message)
+ end
+ end
end
- def add_tag(tag_name, committer:, target:, message: nil)
- target_object = Ref.dereference_object(lookup(target))
- raise InvalidRef.new("target not found: #{target}") unless target_object
+ def rm_branch(branch_name, user:)
+ gitaly_migrate(:operation_user_delete_branch) do |is_enabled|
+ if is_enabled
+ gitaly_operations_client.user_delete_branch(branch_name, user)
+ else
+ OperationService.new(user, self).rm_branch(find_branch(branch_name))
+ end
+ end
+ end
- committer = Committer.from_user(committer) if committer.is_a?(User)
+ def rm_tag(tag_name, user:)
+ gitaly_migrate(:operation_user_delete_tag) do |is_enabled|
+ if is_enabled
+ gitaly_operations_client.rm_tag(tag_name, user)
+ else
+ Gitlab::Git::OperationService.new(user, self).rm_tag(find_tag(tag_name))
+ end
+ end
+ end
+
+ def find_tag(name)
+ tags.find { |tag| tag.name == name }
+ end
+
+ def merge(user, source_sha, target_branch, message)
+ committer = Gitlab::Git.committer_hash(email: user.email, name: user.name)
+
+ OperationService.new(user, self).with_branch(target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source_sha
+
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
+
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
- options = nil # Use nil, not the empty hash. Rugged cares about this.
- if message
options = {
+ parents: [our_commit, their_commit],
+ tree: merge_index.write_tree(rugged),
message: message,
- tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name)
+ author: committer,
+ committer: committer
}
+
+ commit_id = create_commit(options)
+
+ yield commit_id
+
+ commit_id
end
+ rescue Gitlab::Git::CommitError # when merge_index.conflicts?
+ nil
+ end
- OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options)
+ def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ OperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ ) do |start_commit|
- find_tag(tag_name)
- rescue Rugged::ReferenceError => ex
- raise InvalidRef, ex
+ Gitlab::Git.check_namespace!(commit, start_repository)
+
+ revert_tree_id = check_revert_content(commit, start_commit.sha)
+ raise CreateTreeError unless revert_tree_id
+
+ committer = user_to_committer(user)
+
+ create_commit(message: message,
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [start_commit.sha])
+ end
+ end
+
+ def check_revert_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
+ args << { mainline: 1 } if target_commit.merge_commit?
+
+ revert_index = rugged.revert_commit(*args)
+ return false if revert_index.conflicts?
+
+ tree_id = revert_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
+ def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ OperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ ) do |start_commit|
+
+ Gitlab::Git.check_namespace!(commit, start_repository)
+
+ cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
+ raise CreateTreeError unless cherry_pick_tree_id
+
+ committer = user_to_committer(user)
+
+ create_commit(message: message,
+ author: {
+ email: commit.author_email,
+ name: commit.author_name,
+ time: commit.authored_date
+ },
+ committer: committer,
+ tree: cherry_pick_tree_id,
+ parents: [start_commit.sha])
+ end
end
- def rm_branch(branch_name, committer:)
- OperationService.new(committer, self).rm_branch(find_branch(branch_name))
+ def check_cherry_pick_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
+ args << 1 if target_commit.merge_commit?
+
+ cherry_pick_index = rugged.cherrypick_commit(*args)
+ return false if cherry_pick_index.conflicts?
+
+ tree_id = cherry_pick_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
end
- def rm_tag(tag_name, committer:)
- OperationService.new(committer, self).rm_tag(find_tag(tag_name))
+ def diff_exists?(sha1, sha2)
+ rugged.diff(sha1, sha2).size > 0
end
- def find_tag(name)
- tags.find { |tag| tag.name == name }
+ def user_to_committer(user)
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
+ end
+
+ def create_commit(params = {})
+ params[:message].delete!("\r")
+
+ Rugged::Commit.create(rugged, params)
end
# Delete the specified branch from the repository
@@ -672,9 +838,7 @@ module Gitlab
end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
- message, status = Gitlab::Popen.popen(
- command,
- path) do |stdin|
+ message, status = popen(command, path) do |stdin|
stdin.write(instructions.join)
end
@@ -798,14 +962,18 @@ module Gitlab
end
def with_repo_branch_commit(start_repository, start_branch_name)
- raise "expected Gitlab::Git::Repository, got #{start_repository}" unless start_repository.is_a?(Gitlab::Git::Repository)
+ Gitlab::Git.check_namespace!(start_repository)
return yield nil if start_repository.empty_repo?
if start_repository == self
yield commit(start_branch_name)
else
- sha = start_repository.commit(start_branch_name).sha
+ start_commit = start_repository.commit(start_branch_name)
+
+ return yield nil unless start_commit
+
+ sha = start_commit.sha
if branch_commit = commit(sha)
yield branch_commit
@@ -820,9 +988,9 @@ module Gitlab
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref(
- start_repository.path,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- "refs/tmp/#{SecureRandom.hex}/head"
+ start_repository,
+ source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
+ target_ref: "refs/tmp/#{SecureRandom.hex}"
)
yield commit(sha)
@@ -834,8 +1002,9 @@ module Gitlab
with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit
write_ref(local_ref, commit.sha)
+ true
else
- raise Rugged::ReferenceError, 'source repository is empty'
+ false
end
end
end
@@ -853,13 +1022,27 @@ module Gitlab
end
end
- def write_ref(ref_path, sha)
- rugged.references.create(ref_path, sha, force: true)
+ def write_ref(ref_path, ref)
+ raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
+ raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
+
+ command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z]
+ input = "update #{ref_path}\x00#{ref}\x00\x00"
+ output, status = circuit_breaker.perform do
+ popen(command, path) { |stdin| stdin.write(input) }
+ end
+
+ raise GitError, output unless status.zero?
end
- def fetch_ref(source_path, source_ref, target_ref)
- args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
- message, status = run_git(args)
+ def fetch_ref(source_repository, source_ref:, target_ref:)
+ message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled|
+ if is_enabled
+ gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
+ else
+ local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref)
+ end
+ end
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
@@ -868,9 +1051,9 @@ module Gitlab
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
- def run_git(args)
+ def run_git(args, env: {})
circuit_breaker.perform do
- popen([Gitlab.config.git.bin_path, *args], path)
+ popen([Gitlab.config.git.bin_path, *args], path, env)
end
end
@@ -894,11 +1077,17 @@ module Gitlab
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
- branch_count > 0
+ return @has_visible_content if defined?(@has_visible_content)
+
+ @has_visible_content = has_local_branches?
end
def gitaly_repository
- Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
+ Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
+ end
+
+ def gitaly_operations_client
+ @gitaly_operations_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
def gitaly_ref_client
@@ -913,26 +1102,35 @@ module Gitlab
@gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
end
- def gitaly_migrate(method, &block)
- Gitlab::GitalyClient.migrate(method, &block)
+ def gitaly_operation_client
+ @gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
+ end
+
+ def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
raise NoRepository.new(e)
rescue GRPC::BadStatus => e
raise CommandError.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
end
private
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
- branches = rugged.branches.each(filter).map do |rugged_ref|
- begin
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- rescue Rugged::ReferenceError
- # Omit invalid branch
- end
- end.compact
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
+ branches = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ rugged.branches.each(filter).map do |rugged_ref|
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end.compact
+ end
sort_branches(branches, sort_by)
end
@@ -1232,6 +1430,33 @@ module Gitlab
false
end
+ def gitaly_add_tag(tag_name, user:, target:, message: nil)
+ gitaly_operations_client.add_tag(tag_name, user, target, message)
+ end
+
+ def rugged_add_tag(tag_name, user:, target:, message: nil)
+ target_object = Ref.dereference_object(lookup(target))
+ raise InvalidRef.new("target not found: #{target}") unless target_object
+
+ user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
+
+ options = nil # Use nil, not the empty hash. Rugged cares about this.
+ if message
+ options = {
+ message: message,
+ tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name)
+ }
+ end
+
+ Gitlab::Git::OperationService.new(user, self).add_tag(tag_name, target_object.oid, options)
+
+ find_tag(tag_name)
+ rescue Rugged::ReferenceError => ex
+ raise InvalidRef, ex
+ rescue Rugged::TagError
+ raise TagExistsError
+ end
+
def rugged_create_branch(ref, start_point)
rugged_ref = rugged.branches.create(ref, start_point)
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
@@ -1274,6 +1499,46 @@ module Gitlab
file.write(gitattributes_content)
end
end
+
+ def gitaly_add_branch(branch_name, user, target)
+ gitaly_operation_client.user_create_branch(branch_name, user, target)
+ rescue GRPC::FailedPrecondition => ex
+ raise InvalidRef, ex
+ end
+
+ def rugged_add_branch(branch_name, user, target)
+ target_object = Ref.dereference_object(lookup(target))
+ raise InvalidRef.new("target not found: #{target}") unless target_object
+
+ OperationService.new(user, self).add_branch(branch_name, target_object.oid)
+ find_branch(branch_name)
+ rescue Rugged::ReferenceError => ex
+ raise InvalidRef, ex
+ end
+
+ def local_fetch_ref(source_path, source_ref:, target_ref:)
+ args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
+ run_git(args)
+ end
+
+ def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
+ gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
+ gitaly_address = gitaly_resolver.address(source_repository.storage)
+ gitaly_token = gitaly_resolver.token(source_repository.storage)
+
+ request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository)
+ env = {
+ 'GITALY_ADDRESS' => gitaly_address,
+ 'GITALY_PAYLOAD' => request.to_json,
+ 'GITALY_WD' => Dir.pwd,
+ 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
+ }
+ env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
+
+ args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
+
+ run_git(args, env: env)
+ end
end
end
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 2b5785a1f08..92a6a672534 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -3,6 +3,8 @@
module Gitlab
module Git
class RevList
+ include Gitlab::Git::Popen
+
attr_reader :oldrev, :newrev, :path_to_repo
def initialize(path_to_repo:, newrev:, oldrev: nil)
@@ -26,10 +28,10 @@ module Gitlab
private
def execute(args)
- output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
+ output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero?
- raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
end
output.split("\n")
diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb
index e28be4b8a38..08e6c29abad 100644
--- a/lib/gitlab/git/storage.rb
+++ b/lib/gitlab/git/storage.rb
@@ -11,6 +11,7 @@ module Gitlab
end
CircuitOpen = Class.new(Inaccessible)
+ Misconfiguration = Class.new(Inaccessible)
REDIS_KEY_PREFIX = 'storage_accessible:'.freeze
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index 9ea9367d4b7..1eaa2d83fb6 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -28,14 +28,26 @@ module Gitlab
def self.for_storage(storage)
cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do
Hash.new do |hash, storage_name|
- hash[storage_name] = new(storage_name)
+ hash[storage_name] = build(storage_name)
end
end
cached_circuitbreakers[storage]
end
- def initialize(storage, hostname = Gitlab::Environment.hostname)
+ def self.build(storage, hostname = Gitlab::Environment.hostname)
+ config = Gitlab.config.repositories.storages[storage]
+
+ if !config.present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
+ elsif !config['path'].present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
+ else
+ new(storage, hostname)
+ end
+ end
+
+ def initialize(storage, hostname)
@storage = storage
@hostname = hostname
@@ -64,6 +76,10 @@ module Gitlab
recent_failure || too_many_failures
end
+ def failure_info
+ @failure_info ||= get_failure_info
+ end
+
# Memoizing the `storage_available` call means we only do it once per
# request when the storage is available.
#
@@ -121,10 +137,12 @@ module Gitlab
end
end
- def failure_info
- @failure_info ||= get_failure_info
+ def cache_key
+ @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
end
+ private
+
def get_failure_info
last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
redis.hmget(cache_key, :last_failure, :failure_count)
@@ -134,10 +152,6 @@ module Gitlab
FailureInfo.new(last_failure, failure_count.to_i)
end
-
- def cache_key
- @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
- end
end
end
end
diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb
index 2d723147f4f..1564e94b7f7 100644
--- a/lib/gitlab/git/storage/health.rb
+++ b/lib/gitlab/git/storage/health.rb
@@ -78,7 +78,7 @@ module Gitlab
def failing_circuit_breakers
@failing_circuit_breakers ||= failing_on_hosts.map do |hostname|
- CircuitBreaker.new(storage_name, hostname)
+ CircuitBreaker.build(storage_name, hostname)
end
end
diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb
new file mode 100644
index 00000000000..297c043d054
--- /dev/null
+++ b/lib/gitlab/git/storage/null_circuit_breaker.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Git
+ module Storage
+ class NullCircuitBreaker
+ # These will have actual values
+ attr_reader :storage,
+ :hostname
+
+ # These will always have nil values
+ attr_reader :storage_path,
+ :failure_wait_time,
+ :failure_reset_time,
+ :storage_timeout
+
+ def initialize(storage, hostname, error: nil)
+ @storage = storage
+ @hostname = hostname
+ @error = error
+ end
+
+ def perform
+ @error ? raise(@error) : yield
+ end
+
+ def circuit_broken?
+ !!@error
+ end
+
+ def failure_count_threshold
+ 1
+ end
+
+ def last_failure
+ circuit_broken? ? Time.now : nil
+ end
+
+ def failure_count
+ circuit_broken? ? 1 : 0
+ end
+
+ def failure_info
+ Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(last_failure, failure_count)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/user.rb b/lib/gitlab/git/user.rb
new file mode 100644
index 00000000000..da74719ae87
--- /dev/null
+++ b/lib/gitlab/git/user.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Git
+ class User
+ attr_reader :username, :name, :email, :gl_id
+
+ def self.from_gitlab(gitlab_user)
+ new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
+ end
+
+ # TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628
+ def self.from_gitaly(gitaly_user)
+ new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
+ end
+
+ def initialize(username, name, email, gl_id)
+ @username = username
+ @name = name
+ @email = email
+ @gl_id = gl_id
+ end
+
+ def ==(other)
+ [username, name, email, gl_id] == [other.username, other.name, other.email, other.gl_id]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
new file mode 100644
index 00000000000..d651c931a38
--- /dev/null
+++ b/lib/gitlab/git/wiki.rb
@@ -0,0 +1,115 @@
+module Gitlab
+ module Git
+ class Wiki
+ DuplicatePageError = Class.new(StandardError)
+
+ CommitDetails = Struct.new(:name, :email, :message) do
+ def to_h
+ { name: name, email: email, message: message }
+ end
+ end
+
+ def self.default_ref
+ 'master'
+ end
+
+ # Initialize with a Gitlab::Git::Repository instance
+ def initialize(repository)
+ @repository = repository
+ end
+
+ def repository_exists?
+ @repository.exists?
+ end
+
+ def write_page(name, format, content, commit_details)
+ assert_type!(format, Symbol)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.write_page(name, format, content, commit_details.to_h)
+
+ nil
+ rescue Gollum::DuplicatePageError => e
+ raise Gitlab::Git::Wiki::DuplicatePageError, e.message
+ end
+
+ def delete_page(page_path, commit_details)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
+ nil
+ end
+
+ def update_page(page_path, title, format, content, commit_details)
+ assert_type!(format, Symbol)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
+ nil
+ end
+
+ def pages
+ gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
+ end
+
+ def page(title:, version: nil, dir: nil)
+ if version
+ version = Gitlab::Git::Commit.find(@repository, version).id
+ end
+
+ gollum_page = gollum_wiki.page(title, version, dir)
+ return unless gollum_page
+
+ new_page(gollum_page)
+ end
+
+ def file(name, version)
+ version ||= self.class.default_ref
+ gollum_file = gollum_wiki.file(name, version)
+ return unless gollum_file
+
+ Gitlab::Git::WikiFile.new(gollum_file)
+ end
+
+ def page_versions(page_path)
+ current_page = gollum_page_by_path(page_path)
+ current_page.versions.map do |gollum_git_commit|
+ gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id)
+ new_version(gollum_page, gollum_git_commit.id)
+ end
+ end
+
+ def preview_slug(title, format)
+ gollum_wiki.preview_page(title, '', format).url_path
+ end
+
+ private
+
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
+ def gollum_page_by_path(page_path)
+ page_name = Gollum::Page.canonicalize_filename(page_path)
+ page_dir = File.split(page_path).first
+
+ gollum_wiki.paged(page_name, page_dir)
+ end
+
+ def new_page(gollum_page)
+ Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
+ end
+
+ def new_version(gollum_page, commit_id)
+ commit = Gitlab::Git::Commit.find(@repository, commit_id)
+ Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format)
+ end
+
+ def assert_type!(object, klass)
+ unless object.is_a?(klass)
+ raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb
new file mode 100644
index 00000000000..527f2a44dea
--- /dev/null
+++ b/lib/gitlab/git/wiki_file.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Git
+ class WikiFile
+ attr_reader :mime_type, :raw_data, :name
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Because Gollum::File is not serializable we must get all the data from
+ # 'gollum_file' during initialization, and NOT store it in an instance
+ # variable.
+ def initialize(gollum_file)
+ @mime_type = gollum_file.mime_type
+ @raw_data = gollum_file.raw_data
+ @name = gollum_file.name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb
new file mode 100644
index 00000000000..a06bac4414f
--- /dev/null
+++ b/lib/gitlab/git/wiki_page.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module Git
+ class WikiPage
+ attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Because Gollum::Page is not serializable we must get all the data from
+ # 'gollum_page' during initialization, and NOT store it in an instance
+ # variable.
+ #
+ # Note that 'version' is a WikiPageVersion instance which it itself
+ # serializable. That means it's OK to store 'version' in an instance
+ # variable.
+ def initialize(gollum_page, version)
+ @url_path = gollum_page.url_path
+ @title = gollum_page.title
+ @format = gollum_page.format
+ @path = gollum_page.path
+ @raw_data = gollum_page.raw_data
+ @name = gollum_page.name
+ @historical = gollum_page.historical?
+
+ @version = version
+ end
+
+ def historical?
+ @historical
+ end
+
+ def text_data
+ return @text_data if defined?(@text_data)
+
+ @text_data = @raw_data && Gitlab::EncodingHelper.encode!(@raw_data.dup)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_page_version.rb b/lib/gitlab/git/wiki_page_version.rb
new file mode 100644
index 00000000000..55f1afedcab
--- /dev/null
+++ b/lib/gitlab/git/wiki_page_version.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Git
+ class WikiPageVersion
+ attr_reader :commit, :format
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Both 'commit' (a Gitlab::Git::Commit) and 'format' (a string) are
+ # serializable.
+ def initialize(commit, format)
+ @commit = commit
+ @format = format
+ end
+
+ delegate :message, :sha, :id, :author_name, :authored_date, to: :commit
+ end
+ end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 62d1ecae676..42b59c106e2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -16,7 +16,9 @@ module Gitlab
account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
- receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.'
+ receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
+ read_only: 'The repository is temporarily read-only. Please try again later.',
+ cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
@@ -159,6 +161,14 @@ module Gitlab
end
def check_push_access!(changes)
+ if project.repository_read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
+ end
+
if deploy_key
check_deploy_key_push_access!
elsif user
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 1fe5155c093..98f1f45b338 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,7 @@
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
+ read_only: "You can't push code to a read-only GitLab instance.",
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
@@ -17,6 +18,10 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
true
end
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index a3c6b21a6a1..2e3e4fc3f1f 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -11,7 +11,7 @@ module Gitlab
return false if ref_name.start_with?('refs/remotes/')
Gitlab::Utils.system_silent(
- %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
+ %W(#{Gitlab.config.git.bin_path} check-ref-format --branch #{ref_name}))
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index a3dc2cd0b60..cf36106e23d 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -10,7 +10,25 @@ module Gitlab
OPT_OUT = 3
end
+ class TooManyInvocationsError < StandardError
+ attr_reader :call_site, :invocation_count, :max_call_stack
+
+ def initialize(call_site, invocation_count, max_call_stack, most_invoked_stack)
+ @call_site = call_site
+ @invocation_count = invocation_count
+ @max_call_stack = max_call_stack
+ stacks = most_invoked_stack.join('\n') if most_invoked_stack
+
+ msg = "GitalyClient##{call_site} called #{invocation_count} times from single request. Potential n+1?"
+ msg << "\nThe following call site called into Gitaly #{max_call_stack} times:\n#{stacks}\n" if stacks
+
+ super(msg)
+ end
+ end
+
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
+ MAXIMUM_GITALY_CALLS = 30
+ CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
MUTEX = Mutex.new
private_constant :MUTEX
@@ -53,6 +71,8 @@ module Gitlab
# All Gitaly RPC call sites should use GitalyClient.call. This method
# makes sure that per-request authentication headers are set.
def self.call(storage, service, rpc, request)
+ enforce_gitaly_request_limits(:call)
+
metadata = request_metadata(storage)
metadata = yield(metadata) if block_given?
stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend
@@ -60,7 +80,16 @@ module Gitlab
def self.request_metadata(storage)
encoded_token = Base64.strict_encode64(token(storage).to_s)
- { metadata: { 'authorization' => "Bearer #{encoded_token}" } }
+ metadata = {
+ 'authorization' => "Bearer #{encoded_token}",
+ 'client_name' => CLIENT_NAME
+ }
+
+ feature_stack = Thread.current[:gitaly_feature_stack]
+ feature = feature_stack && feature_stack[0]
+ metadata['call_site'] = feature.to_s if feature
+
+ { metadata: metadata }
end
def self.token(storage)
@@ -107,12 +136,107 @@ module Gitlab
private_class_method :opt_into_all_features?
def self.migrate(feature, status: MigrationStatus::OPT_IN)
+ # Enforce limits at both the `migrate` and `call` sites to ensure that
+ # problems are not hidden by a feature being disabled
+ enforce_gitaly_request_limits(:migrate)
+
is_enabled = feature_enabled?(feature, status: status)
metric_name = feature.to_s
metric_name += "_gitaly" if is_enabled
Gitlab::Metrics.measure(metric_name) do
- yield is_enabled
+ # Some migrate calls wrap other migrate calls
+ allow_n_plus_1_calls do
+ feature_stack = Thread.current[:gitaly_feature_stack] ||= []
+ feature_stack.unshift(feature)
+ begin
+ yield is_enabled
+ ensure
+ feature_stack.shift
+ Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
+ end
+ end
+ end
+ end
+
+ # Ensures that Gitaly is not being abuse through n+1 misuse etc
+ def self.enforce_gitaly_request_limits(call_site)
+ # Only count limits in request-response environments (not sidekiq for example)
+ return unless RequestStore.active?
+
+ # This is this actual number of times this call was made. Used for information purposes only
+ actual_call_count = increment_call_count("gitaly_#{call_site}_actual")
+
+ # Do no enforce limits in production
+ return if Rails.env.production? || ENV["GITALY_DISABLE_REQUEST_LIMITS"]
+
+ # Check if this call is nested within a allow_n_plus_1_calls
+ # block and skip check if it is
+ return if get_call_count(:gitaly_call_count_exception_block_depth) > 0
+
+ # This is the count of calls outside of a `allow_n_plus_1_calls` block
+ # It is used for enforcement but not statistics
+ permitted_call_count = increment_call_count("gitaly_#{call_site}_permitted")
+
+ count_stack
+
+ return if permitted_call_count <= MAXIMUM_GITALY_CALLS
+
+ raise TooManyInvocationsError.new(call_site, actual_call_count, max_call_count, max_stacks)
+ end
+
+ def self.allow_n_plus_1_calls
+ return yield unless RequestStore.active?
+
+ begin
+ increment_call_count(:gitaly_call_count_exception_block_depth)
+ yield
+ ensure
+ decrement_call_count(:gitaly_call_count_exception_block_depth)
+ end
+ end
+
+ def self.get_call_count(key)
+ RequestStore.store[key] || 0
+ end
+ private_class_method :get_call_count
+
+ def self.increment_call_count(key)
+ RequestStore.store[key] ||= 0
+ RequestStore.store[key] += 1
+ end
+ private_class_method :increment_call_count
+
+ def self.decrement_call_count(key)
+ RequestStore.store[key] -= 1
+ end
+ private_class_method :decrement_call_count
+
+ # Returns an estimate of the number of Gitaly calls made for this
+ # request
+ def self.get_request_count
+ return 0 unless RequestStore.active?
+
+ gitaly_migrate_count = get_call_count("gitaly_migrate_actual")
+ gitaly_call_count = get_call_count("gitaly_call_actual")
+
+ # Using the maximum of migrate and call_count will provide an
+ # indicator of how many Gitaly calls will be made, even
+ # before a feature is enabled. This provides us with a single
+ # metric, but not an exact number, but this tradeoff is acceptable
+ if gitaly_migrate_count > gitaly_call_count
+ gitaly_migrate_count
+ else
+ gitaly_call_count
+ end
+ end
+
+ def self.reset_counts
+ return unless RequestStore.active?
+
+ %w[migrate call].each do |call_site|
+ RequestStore.store["gitaly_#{call_site}_actual"] = 0
+ RequestStore.store["gitaly_#{call_site}_permitted"] = 0
end
end
@@ -121,8 +245,56 @@ module Gitlab
path.read.chomp
end
+ def self.timestamp(t)
+ Google::Protobuf::Timestamp.new(seconds: t.to_i)
+ end
+
def self.encode(s)
+ return "" if s.nil?
+
s.dup.force_encoding(Encoding::ASCII_8BIT)
end
+
+ def self.encode_repeated(a)
+ Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
+ end
+
+ # Count a stack. Used for n+1 detection
+ def self.count_stack
+ return unless RequestStore.active?
+
+ stack_string = caller.drop(1).join("\n")
+
+ RequestStore.store[:stack_counter] ||= Hash.new
+
+ count = RequestStore.store[:stack_counter][stack_string] || 0
+ RequestStore.store[:stack_counter][stack_string] = count + 1
+ end
+ private_class_method :count_stack
+
+ # Returns a count for the stack which called Gitaly the most times. Used for n+1 detection
+ def self.max_call_count
+ return 0 unless RequestStore.active?
+
+ stack_counter = RequestStore.store[:stack_counter]
+ return 0 unless stack_counter
+
+ stack_counter.values.max
+ end
+ private_class_method :max_call_count
+
+ # Returns the stacks that calls Gitaly the most times. Used for n+1 detection
+ def self.max_stacks
+ return nil unless RequestStore.active?
+
+ stack_counter = RequestStore.store[:stack_counter]
+ return nil unless stack_counter
+
+ max = max_call_count
+ return nil if max.zero?
+
+ stack_counter.select { |_, v| v == max }.keys
+ end
+ private_class_method :max_stacks
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0825a3a7694..a2b50f2507e 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -32,20 +32,38 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request).value
end
+ def diff(from, to, options = {})
+ from_id = case from
+ when NilClass
+ EMPTY_TREE_ID
+ when Rugged::Commit
+ from.oid
+ else
+ from
+ end
+
+ to_id = case to
+ when NilClass
+ EMPTY_TREE_ID
+ when Rugged::Commit
+ to.oid
+ else
+ to
+ end
+
+ request_params = diff_between_commits_request_params(from_id, to_id, options)
+
+ call_commit_diff(request_params, options)
+ end
+
def diff_from_parent(commit, options = {})
- request_params = commit_diff_request_params(commit, options)
- request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
- request_params[:enforce_limits] = options.fetch(:limits, true)
- request_params[:collapse_diffs] = request_params[:enforce_limits] || !options.fetch(:expanded, true)
- request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
+ request_params = diff_from_parent_request_params(commit, options)
- request = Gitaly::CommitDiffRequest.new(request_params)
- response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
- GitalyClient::DiffStitcher.new(response)
+ call_commit_diff(request_params, options)
end
def commit_deltas(commit)
- request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit))
+ request = Gitaly::CommitDeltaRequest.new(diff_from_parent_request_params(commit))
response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request)
response.flat_map { |msg| msg.deltas }
@@ -204,16 +222,59 @@ module Gitlab
response.sum(&:data)
end
+ def commit_stats(revision)
+ request = Gitaly::CommitStatsRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+ GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
+ end
+
+ def find_commits(options)
+ request = Gitaly::FindCommitsRequest.new(
+ repository: @gitaly_repo,
+ limit: options[:limit],
+ offset: options[:offset],
+ follow: options[:follow],
+ skip_merges: options[:skip_merges],
+ disable_walk: options[:disable_walk]
+ )
+ request.after = GitalyClient.timestamp(options[:after]) if options[:after]
+ request.before = GitalyClient.timestamp(options[:before]) if options[:before]
+ request.revision = GitalyClient.encode(options[:ref]) if options[:ref]
+
+ request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request)
+
+ consume_commits_response(response)
+ end
+
private
- def commit_diff_request_params(commit, options = {})
+ def call_commit_diff(request_params, options = {})
+ request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
+ request_params[:enforce_limits] = options.fetch(:limits, true)
+ request_params[:collapse_diffs] = request_params[:enforce_limits] || !options.fetch(:expanded, true)
+ request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
+
+ request = Gitaly::CommitDiffRequest.new(request_params)
+ response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
+ GitalyClient::DiffStitcher.new(response)
+ end
+
+ def diff_from_parent_request_params(commit, options = {})
parent_id = commit.parent_ids.first || EMPTY_TREE_ID
+ diff_between_commits_request_params(parent_id, commit.id, options)
+ end
+
+ def diff_between_commits_request_params(from_id, to_id, options)
{
repository: @gitaly_repo,
- left_commit_id: parent_id,
- right_commit_id: commit.id,
- paths: options.fetch(:paths, [])
+ left_commit_id: from_id,
+ right_commit_id: to_id,
+ paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) }
}
end
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
new file mode 100644
index 00000000000..bd7c345ac01
--- /dev/null
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module GitalyClient
+ class NamespaceService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ def exists?(name)
+ request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:namespace_exists, request).exists
+ end
+
+ def add(name)
+ request = Gitaly::AddNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:add_namespace, request)
+ end
+
+ def remove(name)
+ request = Gitaly::RemoveNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:remove_namespace, request)
+ end
+
+ def rename(from, to)
+ request = Gitaly::RenameNamespaceRequest.new(storage_name: @storage, from: from, to: to)
+
+ gitaly_client_call(:rename_namespace, request)
+ end
+
+ private
+
+ def gitaly_client_call(type, request)
+ GitalyClient.call(@storage, :namespace_service, type, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
new file mode 100644
index 00000000000..81ddaf13e10
--- /dev/null
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -0,0 +1,79 @@
+module Gitlab
+ module GitalyClient
+ class OperationService
+ def initialize(repository)
+ @gitaly_repo = repository.gitaly_repository
+ @repository = repository
+ end
+
+ def rm_tag(tag_name, user)
+ request = Gitaly::UserDeleteTagRequest.new(
+ repository: @gitaly_repo,
+ tag_name: GitalyClient.encode(tag_name),
+ user: Util.gitaly_user(user)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request)
+
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ end
+ end
+
+ def add_tag(tag_name, user, target, message)
+ request = Gitaly::UserCreateTagRequest.new(
+ repository: @gitaly_repo,
+ user: Util.gitaly_user(user),
+ tag_name: GitalyClient.encode(tag_name),
+ target_revision: GitalyClient.encode(target),
+ message: GitalyClient.encode(message.to_s)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request)
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ elsif response.exists
+ raise Gitlab::Git::Repository::TagExistsError
+ end
+
+ Util.gitlab_tag_from_gitaly_tag(@repository, response.tag)
+ rescue GRPC::FailedPrecondition => e
+ raise Gitlab::Git::Repository::InvalidRef, e
+ end
+
+ def user_create_branch(branch_name, user, start_point)
+ request = Gitaly::UserCreateBranchRequest.new(
+ repository: @gitaly_repo,
+ branch_name: GitalyClient.encode(branch_name),
+ user: Util.gitaly_user(user),
+ start_point: GitalyClient.encode(start_point)
+ )
+ response = GitalyClient.call(@repository.storage, :operation_service,
+ :user_create_branch, request)
+ if response.pre_receive_error.present?
+ raise Gitlab::Git::HooksService::PreReceiveError.new(response.pre_receive_error)
+ end
+
+ branch = response.branch
+ return nil unless branch
+
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
+ end
+
+ def user_delete_branch(branch_name, user)
+ request = Gitaly::UserDeleteBranchRequest.new(
+ repository: @gitaly_repo,
+ branch_name: GitalyClient.encode(branch_name),
+ user: Util.gitaly_user(user)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
+
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8ef873d5848..8214b7d63fa 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -57,6 +57,14 @@ module Gitlab
branch_names.count
end
+ # TODO implement a more efficient RPC for this https://gitlab.com/gitlab-org/gitaly/issues/616
+ def has_local_branches?
+ request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request).first
+
+ response&.names.present?
+ end
+
def local_branches(sort_by: nil)
request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo)
request.sort_by = sort_by_param(sort_by) if sort_by
@@ -155,19 +163,7 @@ module Gitlab
def consume_tags_response(response)
response.flat_map do |message|
- message.tags.map do |gitaly_tag|
- if gitaly_tag.target_commit.present?
- gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit)
- end
-
- Gitlab::Git::Tag.new(
- @repository,
- encode!(gitaly_tag.name.dup),
- gitaly_tag.id,
- gitaly_commit,
- encode!(gitaly_tag.message.chomp)
- )
- end
+ message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) }
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 177a1284f38..fdf912214e0 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -53,6 +53,11 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :fetch_remote, request)
end
+
+ def create_repository
+ request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo)
+ GitalyClient.call(@storage, :repository_service, :create_repository, request)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index 8fc937496af..da43c616b94 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -2,14 +2,39 @@ module Gitlab
module GitalyClient
module Util
class << self
- def repository(repository_storage, relative_path)
+ def repository(repository_storage, relative_path, gl_repository)
Gitaly::Repository.new(
storage_name: repository_storage,
relative_path: relative_path,
+ gl_repository: gl_repository,
git_object_directory: Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].to_s,
git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
)
end
+
+ def gitaly_user(gitlab_user)
+ return unless gitlab_user
+
+ Gitaly::User.new(
+ gl_id: Gitlab::GlId.gl_id(gitlab_user),
+ name: GitalyClient.encode(gitlab_user.name),
+ email: GitalyClient.encode(gitlab_user.email)
+ )
+ end
+
+ def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
+ if gitaly_tag.target_commit.present?
+ commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
+ end
+
+ Gitlab::Git::Tag.new(
+ repository,
+ Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup),
+ gitaly_tag.id,
+ commit,
+ Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 9bcc579278f..3a666c2268b 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -20,6 +20,7 @@ module Gitlab
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
+ gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg')
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 025f826e65f..413872d7e08 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -34,6 +34,21 @@ module Gitlab
end
end
+ def subkeys_from_key(key)
+ using_tmp_keychain do
+ fingerprints = CurrentKeyChain.fingerprints_from_key(key)
+ raw_keys = GPGME::Key.find(:public, fingerprints)
+
+ raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
+ primary_subkey_id = raw_key.primary_subkey.keyid
+
+ grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
+ { keyid: s.keyid, fingerprint: s.fingerprint }
+ end
+ end
+ end
+ end
+
def user_infos_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
@@ -69,11 +84,17 @@ module Gitlab
def optimistic_using_tmp_keychain
previous_dir = current_home_dir
- Dir.mktmpdir do |dir|
- GPGME::Engine.home_dir = dir
- yield
- end
+ tmp_dir = Dir.mktmpdir
+ GPGME::Engine.home_dir = tmp_dir
+ yield
ensure
+ # Ignore any errors when removing the tmp directory, as we may run into a
+ # race condition:
+ # The `gpg-agent` agent process may clean up some files as well while
+ # `FileUtils.remove_entry` is iterating the directory and removing all
+ # its contained files and directories recursively, which could raise an
+ # error.
+ FileUtils.remove_entry(tmp_dir, true)
GPGME::Engine.home_dir = previous_dir
end
end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 86bd9f5b125..0f4ba6f83fc 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -43,7 +43,9 @@ module Gitlab
# key belonging to the keyid.
# This way we can add the key to the temporary keychain and extract
# the proper signature.
- gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint)
+ # NOTE: the invoked method is #fingerprint but it's only returning
+ # 16 characters (the format used by keyid) instead of 40.
+ gpg_key = find_gpg_key(verified_signature.fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
@@ -74,7 +76,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
- gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
+ gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
@@ -98,6 +100,10 @@ module Gitlab
def user_infos(gpg_key)
gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
end
+
+ def find_gpg_key(keyid)
+ GpgKey.find_by(primary_keyid: keyid) || GpgKeySubkey.find_by(keyid: keyid)
+ end
end
end
end
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index e085eab26c9..1991911ef6a 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -9,8 +9,8 @@ module Gitlab
GpgSignature
.select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
- .where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
- .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
+ .where(gpg_key_primary_keyid: @gpg_key.keyids)
+ .find_each { |sig| sig.gpg_commit&.update_signature!(sig) }
end
end
end
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index 5a31e56cb30..635f52131f9 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -22,7 +22,7 @@ module Gitlab
def base_and_ancestors
return ancestors_base unless Group.supports_nested_groups?
- base_and_ancestors_cte.apply_to(model.all)
+ read_only(base_and_ancestors_cte.apply_to(model.all))
end
# Returns a relation that includes the descendants_base set of groups
@@ -30,7 +30,7 @@ module Gitlab
def base_and_descendants
return descendants_base unless Group.supports_nested_groups?
- base_and_descendants_cte.apply_to(model.all)
+ read_only(base_and_descendants_cte.apply_to(model.all))
end
# Returns a relation that includes the base groups, their ancestors,
@@ -67,11 +67,13 @@ module Gitlab
union = SQL::Union.new([model.unscoped.from(ancestors_table),
model.unscoped.from(descendants_table)])
- model
+ relation = model
.unscoped
.with
.recursive(ancestors.to_arel, descendants.to_arel)
.from("(#{union.to_sql}) #{model.table_name}")
+
+ read_only(relation)
end
private
@@ -107,5 +109,12 @@ module Gitlab
def groups_table
model.arel_table
end
+
+ def read_only(relation)
+ # relations using a CTE are not safe to use with update_all as it will
+ # throw away the CTE, hence we mark them as read-only.
+ relation.extend(Gitlab::Database::ReadOnlyRelation)
+ relation
+ end
end
end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index eef97f54962..afaa59b1018 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -58,11 +58,11 @@ module Gitlab
end
def repository_storages
- @repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages
+ storages_paths.keys
end
def storages_paths
- @storage_paths ||= Gitlab.config.repositories.storages
+ Gitlab.config.repositories.storages
end
def exec_with_timeout(cmd_args, *args, &block)
@@ -125,7 +125,7 @@ module Gitlab
end
def storage_circuitbreaker_test(storage_name)
- Gitlab::Git::Storage::CircuitBreaker.new(storage_name).perform { "OK" }
+ Gitlab::Git::Storage::CircuitBreaker.build(storage_name).perform { "OK" }
rescue Gitlab::Git::Storage::Inaccessible
nil
end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 5d106b5c075..bdc0f04b56b 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -17,7 +17,8 @@ module Gitlab
'it' => 'Italiano',
'uk' => 'УкраїнÑька',
'ja' => '日本語',
- 'ko' => '한국어'
+ 'ko' => '한국어',
+ 'nl_NL' => 'Nederlands'
}.freeze
def available_locales
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2171c6c7bbb..dec8b4c5acd 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -53,6 +53,7 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
+ - :cluster
- :services
- :hooks
- protected_branches:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 380b336395d..a76cf1addc0 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,6 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
+ cluster: 'Gcp::Cluster',
+ clusters: 'Gcp::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index cdbdfa10d0e..da43bd0af4b 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -113,7 +113,7 @@ module Gitlab
def kubeconfig_embed_ca_pem(config, ca_pem)
cluster = config.dig(:clusters, 0, :cluster)
- cluster[:'certificate-authority-data'] = Base64.encode64(ca_pem)
+ cluster[:'certificate-authority-data'] = Base64.strict_encode64(ca_pem)
end
end
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index fb68627dedf..e60ceba27c8 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -16,7 +16,7 @@ module Gitlab
def self.allowed?(user)
self.open(user) do |access|
if access.allowed?
- Users::UpdateService.new(user, last_credential_check_at: Time.now).execute
+ Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
true
else
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index cd7e4ca7b7e..0afaa2306b5 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -22,8 +22,8 @@ module Gitlab
Gitlab::LDAP::Config.new(provider)
end
- def users(field, value, limit = nil)
- options = user_options(field, value, limit)
+ def users(fields, value, limit = nil)
+ options = user_options(Array(fields), value, limit)
entries = ldap_search(options).select do |entry|
entry.respond_to? config.uid
@@ -72,20 +72,24 @@ module Gitlab
private
- def user_options(field, value, limit)
- options = { attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq }
+ def user_options(fields, value, limit)
+ options = {
+ attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq,
+ base: config.base
+ }
+
options[:size] = limit if limit
- if field.to_sym == :dn
+ if fields.include?('dn')
+ raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
+
options[:base] = value
options[:scope] = Net::LDAP::SearchScope_BaseObject
- options[:filter] = user_filter
else
- options[:base] = config.base
- options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value))
+ filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
end
- options
+ options.merge(filter: user_filter(filter))
end
def user_filter(filter = nil)
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 4fbc5fa5262..3123da17fd9 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -3,6 +3,10 @@
module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
+ def uid
+ Gitlab::LDAP::Person.normalize_dn(super)
+ end
+
private
def get_info(key)
diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb
new file mode 100644
index 00000000000..d6142dc6549
--- /dev/null
+++ b/lib/gitlab/ldap/dn.rb
@@ -0,0 +1,301 @@
+# -*- ruby encoding: utf-8 -*-
+
+# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
+#
+# For our purposes, this class is used to normalize DNs in order to allow proper
+# comparison.
+#
+# E.g. DNs should be compared case-insensitively (in basically all LDAP
+# implementations or setups), therefore we downcase every DN.
+
+##
+# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
+# ("Distinguished Name") is a unique identifier for an entry within an LDAP
+# directory. It is made up of a number of other attributes strung together,
+# to identify the entry in the tree.
+#
+# Each attribute that makes up a DN needs to have its value escaped so that
+# the DN is valid. This class helps take care of that.
+#
+# A fully escaped DN needs to be unescaped when analysing its contents. This
+# class also helps take care of that.
+module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 4d6f8ac79de..38d7a9ba2f5 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -17,6 +17,12 @@ module Gitlab
adapter.user('dn', dn)
end
+ def self.find_by_email(email, adapter)
+ email_fields = adapter.config.attributes['email']
+
+ adapter.user(email_fields, email)
+ end
+
def self.disabled_via_active_directory?(dn, adapter)
adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
end
@@ -30,6 +36,26 @@ module Gitlab
]
end
+ def self.normalize_dn(dn)
+ ::Gitlab::LDAP::DN.new(dn).to_normalized_s
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
+
+ dn
+ end
+
+ # Returns the UID in a normalized form.
+ #
+ # 1. Excess spaces are stripped
+ # 2. The string is downcased (for case-insensitivity)
+ def self.normalize_uid(uid)
+ ::Gitlab::LDAP::DN.normalize_value(uid)
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
+
+ uid
+ end
+
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@@ -52,7 +78,9 @@ module Gitlab
attribute_value(:email)
end
- delegate :dn, to: :entry
+ def dn
+ self.class.normalize_dn(entry.dn)
+ end
private
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 3bf27b37ae6..1793097363e 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -17,41 +17,19 @@ module Gitlab
end
end
- def initialize(auth_hash)
- super
- update_user_attributes
- end
-
def save
super('LDAP')
end
# instance methods
- def gl_user
- @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user
+ def find_user
+ find_by_uid_and_provider || find_by_email || build_new_user
end
def find_by_uid_and_provider
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- def find_by_email
- ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_attribute?(:email)
- end
-
- def update_user_attributes
- if persisted?
- # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
- identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
- identity ||= gl_user.identities.build(provider: auth_hash.provider)
-
- # For a new identity set extern_uid to the LDAP DN
- # For an existing identity with matching email but changed DN, update the DN.
- # For an existing identity with no change in DN, this line changes nothing.
- identity.extern_uid = auth_hash.uid
- end
- end
-
def changed?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 9f432673a6e..344784c866f 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -4,6 +4,15 @@ require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues)
module Gitlab
module MailRoom
+ DEFAULT_CONFIG = {
+ enabled: false,
+ port: 143,
+ ssl: false,
+ start_tls: false,
+ mailbox: 'inbox',
+ idle_timeout: 60
+ }.freeze
+
class << self
def enabled?
config[:enabled] && config[:address]
@@ -22,16 +31,10 @@ module Gitlab
def fetch_config
return {} unless File.exist?(config_file)
- rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
- all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys
-
- config = all_config[:incoming_email] || {}
- config[:enabled] = false if config[:enabled].nil?
- config[:port] = 143 if config[:port].nil?
- config[:ssl] = false if config[:ssl].nil?
- config[:start_tls] = false if config[:start_tls].nil?
- config[:mailbox] = 'inbox' if config[:mailbox].nil?
- config[:idle_timeout] = 60 if config[:idle_timeout].nil?
+ config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] || {}
+ config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval|
+ newval.nil? ? oldval : newval
+ end
if config[:enabled] && config[:address]
gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
@@ -45,6 +48,10 @@ module Gitlab
config
end
+ def rails_env
+ @rails_env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
+ end
+
def config_file
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
end
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
deleted file mode 100644
index 306923902e0..00000000000
--- a/lib/gitlab/markdown/pipeline.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module Gitlab
- module Markdown
- class Pipeline
- def self.[](name)
- name ||= :full
- const_get("#{name.to_s.camelize}Pipeline")
- end
-
- def self.filters
- []
- end
-
- def self.transform_context(context)
- context
- end
-
- def self.html_pipeline
- @html_pipeline ||= HTML::Pipeline.new(filters)
- end
-
- class << self
- %i(call to_document to_html).each do |meth|
- define_method(meth) do |text, context|
- context = transform_context(context)
-
- html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 6023fa1820f..f42168c720e 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -3,6 +3,10 @@
module Gitlab
module Middleware
class Go
+ include ActionView::Helpers::TagHelper
+
+ PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
+
def initialize(app)
@app = app
end
@@ -10,17 +14,20 @@ module Gitlab
def call(env)
request = Rack::Request.new(env)
- if go_request?(request)
- render_go_doc(request)
- else
- @app.call(env)
- end
+ render_go_doc(request) || @app.call(env)
end
private
def render_go_doc(request)
- body = go_body(request)
+ return unless go_request?(request)
+
+ path = project_path(request)
+ return unless path
+
+ body = go_body(path)
+ return unless body
+
response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
response.finish
end
@@ -29,11 +36,13 @@ module Gitlab
request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
end
- def go_body(request)
- project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+ def go_body(path)
+ project_url = URI.join(Gitlab.config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
+ meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git"
+ head_tag = content_tag :head, meta_tag
+ content_tag :html, head_tag
end
def strip_url(url)
@@ -44,6 +53,10 @@ module Gitlab
path_info = request.env["PATH_INFO"]
path_info.sub!(/^\//, '')
+ project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
+ return unless project_path_match
+ path = project_path_match[1]
+
# Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
# In a traditional project with a single namespace, this would denote repo
# `namespace/project` with subpath `path1/path2/../pathN`, but with nested
@@ -51,7 +64,7 @@ module Gitlab
# `path2/../pathN`, for example.
# We find all potential project paths out of the path segments
- path_segments = path_info.split('/')
+ path_segments = path.split('/')
simple_project_path = path_segments.first(2).join('/')
# If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
new file mode 100644
index 00000000000..0de0cddcce4
--- /dev/null
+++ b/lib/gitlab/middleware/read_only.rb
@@ -0,0 +1,88 @@
+module Gitlab
+ module Middleware
+ class ReadOnly
+ DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
+ APPLICATION_JSON = 'application/json'.freeze
+ API_VERSIONS = (3..4)
+
+ def initialize(app)
+ @app = app
+ @whitelisted = internal_routes
+ end
+
+ def call(env)
+ @env = env
+
+ if disallowed_request? && Gitlab::Database.read_only?
+ Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
+ error_message = 'You cannot do writing operations on a read-only GitLab instance'
+
+ if json_request?
+ return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
+ else
+ rack_flash.alert = error_message
+ rack_session['flash'] = rack_flash.to_session_value
+
+ return [301, { 'Location' => last_visited_url }, []]
+ end
+ end
+
+ @app.call(env)
+ end
+
+ private
+
+ def internal_routes
+ API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
+ end
+
+ def disallowed_request?
+ DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
+ end
+
+ def json_request?
+ request.media_type == APPLICATION_JSON
+ end
+
+ def rack_flash
+ @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
+ end
+
+ def rack_session
+ @env['rack.session']
+ end
+
+ def request
+ @env['rack.request'] ||= Rack::Request.new(@env)
+ end
+
+ def last_visited_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
+ end
+
+ def route_hash
+ @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
+ end
+
+ def whitelisted_routes
+ logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
+ end
+
+ def logout_route
+ route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
+ end
+
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
+ end
+
+ def grack_route
+ request.path.end_with?('.git/git-upload-pack')
+ end
+
+ def lfs_route
+ request.path.end_with?('/info/lfs/objects/batch')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 1f331b1e91d..5b5ed449f94 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def provider
- @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
+ @provider ||= auth_hash.provider.to_s
end
def name
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 7704bf715e4..47c2a422387 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -13,6 +13,7 @@ module Gitlab
def initialize(auth_hash)
self.auth_hash = auth_hash
update_profile if sync_profile_from_provider?
+ add_or_update_user_identities
end
def persisted?
@@ -32,7 +33,7 @@ module Gitlab
block_after_save = needs_blocking?
- Users::UpdateService.new(gl_user).execute!
+ Users::UpdateService.new(gl_user, user: gl_user).execute!
gl_user.block if block_after_save
@@ -44,47 +45,56 @@ module Gitlab
end
def gl_user
- @user ||= find_by_uid_and_provider
+ return @gl_user if defined?(@gl_user)
- if auto_link_ldap_user?
- @user ||= find_or_create_ldap_user
- end
+ @gl_user = find_user
+ end
- if signup_enabled?
- @user ||= build_new_user
- end
+ def find_user
+ user = find_by_uid_and_provider
- if external_provider? && @user
- @user.external = true
- end
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
+
+ user.external = true if external_provider? && user
- @user
+ user
end
protected
- def find_or_create_ldap_user
+ def add_or_update_user_identities
+ return unless gl_user
+
+ # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
+ identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
+
+ identity ||= gl_user.identities.build(provider: auth_hash.provider)
+ identity.extern_uid = auth_hash.uid
+
+ if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
+ log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
+ gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
+ end
+ end
+
+ def find_or_build_ldap_user
return unless ldap_person
- # If a corresponding person exists with same uid in a LDAP server,
- # check if the user already has a GitLab account.
user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
if user
- # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account.
log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
- user.identities.find_or_initialize_by(extern_uid: auth_hash.uid, provider: auth_hash.provider)
- else
- log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account."
- user = find_by_uid_and_provider
- if user.nil?
- log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
- user = build_new_user
- end
- log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}."
- user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn)
+ return user
end
- user
+ log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
+ build_new_user
+ end
+
+ def find_by_email
+ return unless auth_hash.has_attribute?(:email)
+
+ ::User.find_by(email: auth_hash.email.downcase)
end
def auto_link_ldap_user?
@@ -108,9 +118,9 @@ module Gitlab
end
def find_ldap_person(auth_hash, adapter)
- by_uid = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter)
- # The `uid` might actually be a DN. Try it next.
- by_uid || Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
+ Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
+ Gitlab::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
+ Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
end
def ldap_config
@@ -152,7 +162,7 @@ module Gitlab
end
def build_new_user
- user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
+ user_params = user_attributes.merge(skip_confirmation: true)
Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
end
diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb
new file mode 100644
index 00000000000..981ef8faa9a
--- /dev/null
+++ b/lib/gitlab/pages.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module Pages
+ VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 7c02c9c5c48..e68160c8faf 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -33,6 +33,7 @@ module Gitlab
explore
favicon.ico
files
+ google_api
groups
health_check
help
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 58f6245579a..bd677ec4bf3 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -65,5 +65,9 @@ module Gitlab
"can contain only lowercase letters, digits, and '-'. " \
"Must start with a letter, and cannot end with '-'"
end
+
+ def build_trace_section_regex
+ @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/.freeze
+ end
end
end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index 0f323a9e8b2..e0a9d1dee77 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -10,41 +10,20 @@ module Gitlab
super('SAML')
end
- def gl_user
- if auto_link_ldap_user?
- @user ||= find_or_create_ldap_user
- end
-
- @user ||= find_by_uid_and_provider
-
- if auto_link_saml_user?
- @user ||= find_by_email
- end
+ def find_user
+ user = find_by_uid_and_provider
- if signup_enabled?
- @user ||= build_new_user
- end
+ user ||= find_by_email if auto_link_saml_user?
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
- if external_users_enabled? && @user
+ if external_users_enabled? && user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- @user.external =
- if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- false
- else
- true
- end
+ user.external = !(auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
end
- @user
- end
-
- def find_by_email
- if auth_hash.has_attribute?(:email)
- user = ::User.find_by(email: auth_hash.email.downcase)
- user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) if user
- user
- end
+ user
end
def changed?
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 81ecdf43ef9..a37112ae5c4 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -65,7 +65,7 @@ module Gitlab
# Init new repository
#
- # storage - project's storage path
+ # storage - project's storage name
# name - project path with namespace
#
# Ex.
@@ -73,7 +73,19 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def add_repository(storage, name)
- Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
+ relative_path = name.dup
+ relative_path << '.git' unless relative_path.end_with?('.git')
+
+ gitaly_migrate(:create_repository) do |is_enabled|
+ if is_enabled
+ repository = Gitlab::Git::Repository.new(storage, relative_path, '')
+ repository.gitaly_repository_client.create_repository
+ true
+ else
+ repo_path = File.join(Gitlab.config.repositories.storages[storage]['path'], relative_path)
+ Gitlab::Git::Repository.create(repo_path, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
+ end
+ end
rescue => err
Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
false
@@ -210,10 +222,18 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def add_namespace(storage, name)
- path = full_path(storage, name)
- FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).add(name)
+ else
+ path = full_path(storage, name)
+ FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ end
+ end
rescue Errno::EEXIST => e
Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Remove directory from repositories storage
@@ -224,7 +244,15 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def rm_namespace(storage, name)
- FileUtils.rm_r(full_path(storage, name), force: true)
+ Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).remove(name)
+ else
+ FileUtils.rm_r(full_path(storage, name), force: true)
+ end
+ end
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Move namespace directory inside repositories storage
@@ -234,9 +262,17 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def mv_namespace(storage, old_name, new_name)
- return false if exists?(storage, new_name) || !exists?(storage, old_name)
+ Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).rename(old_name, new_name)
+ else
+ return false if exists?(storage, new_name) || !exists?(storage, old_name)
- FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ end
+ end
+ rescue GRPC::InvalidArgument
+ false
end
def url_to_repo(path)
@@ -260,7 +296,13 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- File.exist?(full_path(storage, dir_name))
+ Gitlab::GitalyClient.migrate(:namespace_exists) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).exists?(dir_name)
+ else
+ File.exist?(full_path(storage, dir_name))
+ end
+ end
end
protected
@@ -337,6 +379,14 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
+ def gitaly_namespace_client(storage_path)
+ storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
+ value['path'] == storage_path
+ end
+
+ Gitlab::GitalyClient::NamespaceService.new(storage)
+ end
+
def gitaly_migrate(method, &block)
Gitlab::GitalyClient.migrate(method, &block)
rescue GRPC::NotFound, GRPC::BadStatus => e
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index 104280f520a..d7d24eeb37b 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -25,7 +25,7 @@ module Gitlab
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{MAX_RSS}"
- Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"\
+ Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\
"in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index 222021e8802..f30c771837a 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -12,8 +12,9 @@ module Gitlab
#
# Project.where("id IN (#{sql})")
class Union
- def initialize(relations)
+ def initialize(relations, remove_duplicates: true)
@relations = relations
+ @remove_duplicates = remove_duplicates
end
def to_sql
@@ -25,7 +26,11 @@ module Gitlab
@relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
- fragments.join("\nUNION\n")
+ fragments.join("\n#{union_keyword}\n")
+ end
+
+ def union_keyword
+ @remove_duplicates ? 'UNION' : 'UNION ALL'
end
end
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 703adae12cb..1caa791c1be 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,9 @@
module Gitlab
class UrlSanitizer
+ ALLOWED_SCHEMES = %w[http https ssh git].freeze
+
def self.sanitize(content)
- regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
+ regexp = URI::Parser.new.make_regexp(ALLOWED_SCHEMES)
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
@@ -11,21 +13,20 @@ module Gitlab
def self.valid?(url)
return false unless url.present?
- Addressable::URI.parse(url.strip)
+ uri = Addressable::URI.parse(url.strip)
- true
+ ALLOWED_SCHEMES.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError
false
end
def initialize(url, credentials: nil)
- @url = Addressable::URI.parse(url.to_s.strip)
-
%i[user password].each do |symbol|
credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol)
end
@credentials = credentials
+ @url = parse_url(url)
end
def sanitized_url
@@ -49,12 +50,30 @@ module Gitlab
private
+ def parse_url(url)
+ url = url.to_s.strip
+ match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)})
+ raw_credentials = match[1] if match
+
+ if raw_credentials.present?
+ url.sub!("#{raw_credentials}@", '')
+
+ user, password = raw_credentials.split(':')
+ @credentials ||= { user: user.presence, password: password.presence }
+ end
+
+ url = Addressable::URI.parse(url)
+ url.password = password if password.present?
+ url.user = user if user.present?
+ url
+ end
+
def generate_full_url
return @url unless valid_credentials?
@full_url = @url.dup
- @full_url.password = credentials[:password]
- @full_url.user = credentials[:user]
+ @full_url.password = credentials[:password] if credentials[:password].present?
+ @full_url.user = credentials[:user] if credentials[:user].present?
@full_url
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 3cf26625108..3f3ba77d47f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -9,12 +9,28 @@ module Gitlab
def uncached_data
license_usage_data.merge(system_usage_data)
+ .merge(features_usage_data)
+ .merge(components_usage_data)
end
def to_json(force_refresh: false)
data(force_refresh: force_refresh).to_json
end
+ def license_usage_data
+ usage_data = {
+ uuid: current_application_settings.uuid,
+ hostname: Gitlab.config.gitlab.host,
+ version: Gitlab::VERSION,
+ active_user_count: User.active.count,
+ recorded_at: Time.now,
+ mattermost_enabled: Gitlab.config.mattermost.enabled,
+ edition: 'CE'
+ }
+
+ usage_data
+ end
+
def system_usage_data
{
counts: {
@@ -22,12 +38,17 @@ module Gitlab
ci_builds: ::Ci::Build.count,
ci_internal_pipelines: ::Ci::Pipeline.internal.count,
ci_external_pipelines: ::Ci::Pipeline.external.count,
+ ci_pipeline_config_auto_devops: ::Ci::Pipeline.auto_devops_source.count,
+ ci_pipeline_config_repository: ::Ci::Pipeline.repository_source.count,
ci_runners: ::Ci::Runner.count,
ci_triggers: ::Ci::Trigger.count,
ci_pipeline_schedules: ::Ci::PipelineSchedule.count,
+ auto_devops_enabled: ::ProjectAutoDevops.enabled.count,
+ auto_devops_disabled: ::ProjectAutoDevops.disabled.count,
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
+ gcp_clusters: ::Gcp::Cluster.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
@@ -50,18 +71,28 @@ module Gitlab
}
end
- def license_usage_data
- usage_data = {
- uuid: current_application_settings.uuid,
- hostname: Gitlab.config.gitlab.host,
- version: Gitlab::VERSION,
- active_user_count: User.active.count,
- recorded_at: Time.now,
- mattermost_enabled: Gitlab.config.mattermost.enabled,
- edition: 'CE'
+ def features_usage_data
+ features_usage_data_ce
+ end
+
+ def features_usage_data_ce
+ {
+ signup: current_application_settings.signup_enabled?,
+ ldap: Gitlab.config.ldap.enabled,
+ gravatar: current_application_settings.gravatar_enabled?,
+ omniauth: Gitlab.config.omniauth.enabled,
+ reply_by_email: Gitlab::IncomingEmail.enabled?,
+ container_registry: Gitlab.config.registry.enabled,
+ gitlab_shared_runners: Gitlab.config.gitlab_ci.shared_runners_enabled
}
+ end
- usage_data
+ def components_usage_data
+ {
+ gitlab_pages: { enabled: Gitlab.config.pages.enabled, version: Gitlab::Pages::VERSION },
+ git: { version: Gitlab::Git.version },
+ database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version }
+ }
end
def services_usage
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 7a94af2f8f1..f200c694562 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -22,9 +22,9 @@ module Gitlab
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
+ GL_USERNAME: user&.username,
RepoPath: repo_path
}
-
server = {
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
@@ -89,6 +89,13 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty?
+ if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
+ params.merge!(
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'GitalyRepository' => repository.gitaly_repository.to_h
+ )
+ end
+
[
SEND_DATA_HEADER,
"git-archive:#{encode(params)}"
@@ -121,10 +128,10 @@ module Gitlab
]
end
- def send_artifacts_entry(build, path)
+ def send_artifacts_entry(build, entry)
params = {
'Archive' => build.artifacts_file.path,
- 'Entry' => Base64.encode64(path.to_s)
+ 'Entry' => Base64.encode64(entry.to_s)
}
[
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
new file mode 100644
index 00000000000..99a82c849e0
--- /dev/null
+++ b/lib/google_api/auth.rb
@@ -0,0 +1,54 @@
+module GoogleApi
+ class Auth
+ attr_reader :access_token, :redirect_uri, :state
+
+ ConfigMissingError = Class.new(StandardError)
+
+ def initialize(access_token, redirect_uri, state: nil)
+ @access_token = access_token
+ @redirect_uri = redirect_uri
+ @state = state
+ end
+
+ def authorize_url
+ client.auth_code.authorize_url(
+ redirect_uri: redirect_uri,
+ scope: scope,
+ state: state # This is used for arbitary redirection
+ )
+ end
+
+ def get_token(code)
+ ret = client.auth_code.get_token(code, redirect_uri: redirect_uri)
+ return ret.token, ret.expires_at
+ end
+
+ protected
+
+ def scope
+ raise NotImplementedError
+ end
+
+ private
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "google_oauth2" }
+ end
+
+ def client
+ return @client if defined?(@client)
+
+ unless config
+ raise ConfigMissingError
+ end
+
+ @client = ::OAuth2::Client.new(
+ config.app_id,
+ config.app_secret,
+ site: 'https://accounts.google.com',
+ token_url: '/o/oauth2/token',
+ authorize_url: '/o/oauth2/auth'
+ )
+ end
+ end
+end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
new file mode 100644
index 00000000000..a440a3e3562
--- /dev/null
+++ b/lib/google_api/cloud_platform/client.rb
@@ -0,0 +1,88 @@
+require 'google/apis/container_v1'
+
+module GoogleApi
+ module CloudPlatform
+ class Client < GoogleApi::Auth
+ DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
+ SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
+ LEAST_TOKEN_LIFE_TIME = 10.minutes
+
+ class << self
+ def session_key_for_token
+ :cloud_platform_access_token
+ end
+
+ def session_key_for_expires_at
+ :cloud_platform_expires_at
+ end
+
+ def new_session_key_for_redirect_uri
+ SecureRandom.hex.tap do |state|
+ yield session_key_for_redirect_uri(state)
+ end
+ end
+
+ def session_key_for_redirect_uri(state)
+ "cloud_platform_second_redirect_uri_#{state}"
+ end
+ end
+
+ def scope
+ SCOPE
+ end
+
+ def validate_token(expires_at)
+ return false unless access_token
+ return false unless expires_at
+
+ # Making sure that the token will have been still alive during the cluster creation.
+ return false if token_life_time(expires_at) < LEAST_TOKEN_LIFE_TIME
+
+ true
+ end
+
+ def projects_zones_clusters_get(project_id, zone, cluster_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_cluster(project_id, zone, cluster_id)
+ end
+
+ def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ request_body = Google::Apis::ContainerV1::CreateClusterRequest.new(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ service.create_cluster(project_id, zone, request_body)
+ end
+
+ def projects_zones_operations(project_id, zone, operation_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_operation(project_id, zone, operation_id)
+ end
+
+ def parse_operation_id(self_link)
+ m = self_link.match(%r{projects/.*/zones/.*/operations/(.*)})
+ m[1] if m
+ end
+
+ private
+
+ def token_life_time(expires_at)
+ DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc
+ end
+ end
+ end
+end
diff --git a/lib/rspec_flaky/config.rb b/lib/rspec_flaky/config.rb
new file mode 100644
index 00000000000..a17ae55910e
--- /dev/null
+++ b/lib/rspec_flaky/config.rb
@@ -0,0 +1,21 @@
+require 'json'
+
+module RspecFlaky
+ class Config
+ def self.generate_report?
+ ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ end
+
+ def self.suite_flaky_examples_report_path
+ ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/suite-report.json")
+ end
+
+ def self.flaky_examples_report_path
+ ENV['FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/report.json")
+ end
+
+ def self.new_flaky_examples_report_path
+ ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/new-report.json")
+ end
+ end
+end
diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb
index f81fb90e870..6be24014d89 100644
--- a/lib/rspec_flaky/flaky_example.rb
+++ b/lib/rspec_flaky/flaky_example.rb
@@ -9,24 +9,21 @@ module RspecFlaky
line: example.line,
description: example.description,
last_attempts_count: example.attempts,
- flaky_reports: 1)
+ flaky_reports: 0)
else
super
end
end
- def first_flaky_at
- self[:first_flaky_at] || Time.now
- end
-
- def last_flaky_at
- Time.now
- end
+ def update_flakiness!(last_attempts_count: nil)
+ self.first_flaky_at ||= Time.now
+ self.last_flaky_at = Time.now
+ self.flaky_reports += 1
+ self.last_attempts_count = last_attempts_count if last_attempts_count
- def last_flaky_job
- return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
-
- "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
+ self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ end
end
def to_h
diff --git a/lib/rspec_flaky/flaky_examples_collection.rb b/lib/rspec_flaky/flaky_examples_collection.rb
new file mode 100644
index 00000000000..973c95b0212
--- /dev/null
+++ b/lib/rspec_flaky/flaky_examples_collection.rb
@@ -0,0 +1,37 @@
+require 'json'
+
+module RspecFlaky
+ class FlakyExamplesCollection < SimpleDelegator
+ def self.from_json(json)
+ new(JSON.parse(json))
+ end
+
+ def initialize(collection = {})
+ unless collection.is_a?(Hash)
+ raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!"
+ end
+
+ collection_of_flaky_examples =
+ collection.map do |uid, example|
+ [
+ uid,
+ example.is_a?(RspecFlaky::FlakyExample) ? example : RspecFlaky::FlakyExample.new(example)
+ ]
+ end
+
+ super(Hash[collection_of_flaky_examples])
+ end
+
+ def to_report
+ Hash[map { |uid, example| [uid, example.to_h] }].deep_symbolize_keys
+ end
+
+ def -(other)
+ unless other.respond_to?(:key)
+ raise ArgumentError, "`other` must respond to `#key?`, #{other.class} does not!"
+ end
+
+ self.class.new(reject { |uid, _| other.key?(uid) })
+ end
+ end
+end
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index ec2fbd9e36c..4a5bfec9967 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -2,11 +2,15 @@ require 'json'
module RspecFlaky
class Listener
- attr_reader :all_flaky_examples, :new_flaky_examples
-
- def initialize
- @new_flaky_examples = {}
- @all_flaky_examples = init_all_flaky_examples
+ # - suite_flaky_examples: contains all the currently tracked flacky example
+ # for the whole RSpec suite
+ # - flaky_examples: contains the examples detected as flaky during the
+ # current RSpec run
+ attr_reader :suite_flaky_examples, :flaky_examples
+
+ def initialize(suite_flaky_examples_json = nil)
+ @flaky_examples = FlakyExamplesCollection.new
+ @suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json)
end
def example_passed(notification)
@@ -14,29 +18,21 @@ module RspecFlaky
return unless current_example.attempts > 1
- flaky_example_hash = all_flaky_examples[current_example.uid]
-
- all_flaky_examples[current_example.uid] =
- if flaky_example_hash
- FlakyExample.new(flaky_example_hash).tap do |ex|
- ex.last_attempts_count = current_example.attempts
- ex.flaky_reports += 1
- end
- else
- FlakyExample.new(current_example).tap do |ex|
- new_flaky_examples[current_example.uid] = ex
- end
- end
+ flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
+ flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
+
+ flaky_examples[current_example.uid] = flaky_example
end
def dump_summary(_)
- write_report_file(all_flaky_examples, all_flaky_examples_report_path)
+ write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
+ new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
+ Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_report)
- write_report_file(new_flaky_examples, new_flaky_examples_report_path)
+ write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
@@ -46,30 +42,23 @@ module RspecFlaky
private
- def init_all_flaky_examples
- return {} unless File.exist?(all_flaky_examples_report_path)
+ def init_suite_flaky_examples(suite_flaky_examples_json = nil)
+ unless suite_flaky_examples_json
+ return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
- all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
+ suite_flaky_examples_json = File.read(RspecFlaky::Config.suite_flaky_examples_report_path)
+ end
- Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
+ FlakyExamplesCollection.from_json(suite_flaky_examples_json)
end
- def write_report_file(examples, file_path)
- return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ def write_report_file(examples_collection, file_path)
+ return unless RspecFlaky::Config.generate_report?
report_path_dir = File.dirname(file_path)
FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
- File.write(file_path, JSON.pretty_generate(to_report(examples)))
- end
-
- def all_flaky_examples_report_path
- @all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/all-report.json")
- end
- def new_flaky_examples_report_path
- @new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/new-report.json")
+ File.write(file_path, JSON.pretty_generate(examples_collection.to_report))
end
end
end
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index 7b486d78cf0..9af21078403 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -5,15 +5,16 @@ module SystemCheck
# whitelisted as it may change the SSH client's behaviour dramatically.
WHITELIST = %w[
authorized_keys
+ authorized_keys.lock
authorized_keys2
known_hosts
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (git user is not present or configured)'
+ set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
def skip?
- !home_dir || !File.directory?(home_dir)
+ Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb
index c388682dfb4..6ee8c8874ec 100644
--- a/lib/system_check/app/git_version_check.rb
+++ b/lib/system_check/app/git_version_check.rb
@@ -9,7 +9,7 @@ module SystemCheck
end
def self.current_version
- @current_version ||= Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
+ @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%W(#{Gitlab.config.git.bin_path} --version)))
end
def check?
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index fd82f5f8a4a..08a2c495bd4 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -9,7 +9,7 @@ module SystemCheck
end
def self.current_version
- @current_version ||= Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
+ @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%w(ruby --version)))
end
def check?
diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb
index dee108d987b..e55bea86d3f 100644
--- a/lib/system_check/incoming_email/imap_authentication_check.rb
+++ b/lib/system_check/incoming_email/imap_authentication_check.rb
@@ -4,22 +4,17 @@ module SystemCheck
set_name 'IMAP server credentials are correct?'
def check?
- if mailbox_config
- begin
- imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
- imap.starttls if config[:start_tls]
- imap.login(config[:email], config[:password])
- connected = true
- rescue
- connected = false
- end
+ if config
+ try_connect_imap
+ else
+ @error = "#{mail_room_config_path} does not have mailboxes setup"
+ false
end
-
- connected
end
def show_error
try_fixing_it(
+ "An error occurred: #{@error.class}: #{@error.message}",
'Check that the information in config/gitlab.yml is correct'
)
for_more_information(
@@ -30,15 +25,31 @@ module SystemCheck
private
- def mailbox_config
- return @config if @config
+ def try_connect_imap
+ imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
+ imap.starttls if config[:start_tls]
+ imap.login(config[:email], config[:password])
+ true
+ rescue => error
+ @error = error
+ false
+ end
+
+ def config
+ @config ||= load_config
+ end
+
+ def mail_room_config_path
+ @mail_room_config_path ||=
+ Rails.root.join('config', 'mail_room.yml').to_s
+ end
- config_path = Rails.root.join('config', 'mail_room.yml').to_s
- erb = ERB.new(File.read(config_path))
- erb.filename = config_path
+ def load_config
+ erb = ERB.new(File.read(mail_room_config_path))
+ erb.filename = mail_room_config_path
config_file = YAML.load(erb.result)
- @config = config_file[:mailboxes]&.first
+ config_file.dig(:mailboxes, 0)
end
end
end
diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb
new file mode 100644
index 00000000000..b8446300f72
--- /dev/null
+++ b/lib/system_check/orphans/namespace_check.rb
@@ -0,0 +1,54 @@
+module SystemCheck
+ module Orphans
+ class NamespaceCheck < SystemCheck::BaseCheck
+ set_name 'Orphaned namespaces:'
+
+ def multi_check
+ Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ $stdout.puts
+ $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow)
+ toplevel_namespace_dirs = disk_namespaces(repository_storage['path'])
+
+ orphans = (toplevel_namespace_dirs - existing_namespaces)
+ print_orphans(orphans, storage_name)
+ end
+
+ clear_namespaces! # releases memory when check finishes
+ end
+
+ private
+
+ def print_orphans(orphans, storage_name)
+ if orphans.empty?
+ $stdout.puts "* No orphaned namespaces for #{storage_name} storage".color(:green)
+ return
+ end
+
+ orphans.each do |orphan|
+ $stdout.puts " - #{orphan}".color(:red)
+ end
+ end
+
+ def disk_namespaces(storage_path)
+ fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result|
+ namespace = File.basename(namespace_path)
+ next if namespace.eql?('@hashed')
+
+ result << namespace
+ end
+ end
+
+ def fetch_disk_namespaces(storage_path)
+ Dir.glob(File.join(storage_path, '*'))
+ end
+
+ def existing_namespaces
+ @namespaces ||= Namespace.where(parent: nil).all.pluck(:path)
+ end
+
+ def clear_namespaces!
+ @namespaces = nil
+ end
+ end
+ end
+end
diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb
new file mode 100644
index 00000000000..9b6b2429783
--- /dev/null
+++ b/lib/system_check/orphans/repository_check.rb
@@ -0,0 +1,68 @@
+module SystemCheck
+ module Orphans
+ class RepositoryCheck < SystemCheck::BaseCheck
+ set_name 'Orphaned repositories:'
+ attr_accessor :orphans
+
+ def multi_check
+ Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ $stdout.puts
+ $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow)
+
+ repositories = disk_repositories(repository_storage['path'])
+ orphans = (repositories - fetch_repositories(storage_name))
+
+ print_orphans(orphans, storage_name)
+ end
+ end
+
+ private
+
+ def print_orphans(orphans, storage_name)
+ if orphans.empty?
+ $stdout.puts "* No orphaned repositories for #{storage_name} storage".color(:green)
+ return
+ end
+
+ orphans.each do |orphan|
+ $stdout.puts " - #{orphan}".color(:red)
+ end
+ end
+
+ def disk_repositories(storage_path)
+ fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result|
+ namespace = File.basename(namespace_path)
+ next if namespace.eql?('@hashed')
+
+ fetch_disk_repositories(namespace_path).each do |repo|
+ result << "#{namespace}/#{File.basename(repo)}"
+ end
+ end
+ end
+
+ def fetch_repositories(storage_name)
+ sql = "
+ SELECT
+ CONCAT(n.path, '/', p.path, '.git') repo,
+ CONCAT(n.path, '/', p.path, '.wiki.git') wiki
+ FROM projects p
+ JOIN namespaces n
+ ON (p.namespace_id = n.id AND
+ n.parent_id IS NULL)
+ WHERE (p.repository_storage LIKE ?)
+ "
+
+ query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend
+ ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || []
+ end
+
+ def fetch_disk_namespaces(storage_path)
+ Dir.glob(File.join(storage_path, '*'))
+ end
+
+ def fetch_disk_repositories(namespace_path)
+ Dir.glob(File.join(namespace_path, '*'))
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 259a755d724..a42f02a84fd 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -3,8 +3,8 @@ namespace :gitlab do
desc 'GitLab | Assets | Compile all frontend assets'
task compile: [
'yarn:check',
- 'rake:assets:precompile',
'gettext:po_to_json',
+ 'rake:assets:precompile',
'webpack:compile',
'fix_urls'
]
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 654f638c454..dfade1f3885 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -398,6 +398,35 @@ namespace :gitlab do
end
end
+ namespace :orphans do
+ desc 'Gitlab | Check for orphaned namespaces and repositories'
+ task check: :environment do
+ warn_user_is_not_gitlab
+ checks = [
+ SystemCheck::Orphans::NamespaceCheck,
+ SystemCheck::Orphans::RepositoryCheck
+ ]
+
+ SystemCheck.run('Orphans', checks)
+ end
+
+ desc 'GitLab | Check for orphaned namespaces in the repositories path'
+ task check_namespaces: :environment do
+ warn_user_is_not_gitlab
+ checks = [SystemCheck::Orphans::NamespaceCheck]
+
+ SystemCheck.run('Orphans', checks)
+ end
+
+ desc 'GitLab | Check for orphaned repositories in the repositories path'
+ task check_repositories: :environment do
+ warn_user_is_not_gitlab
+ checks = [SystemCheck::Orphans::RepositoryCheck]
+
+ SystemCheck.run('Orphans', checks)
+ end
+ end
+
namespace :user do
desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args|
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7ccda04a35f..b4d05f5995a 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,10 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_COMMIT_REF_NAME'] }
+ {
+ ce_repo: ENV['CI_REPOSITORY_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME']
+ }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
@@ -13,7 +16,10 @@ namespace :gitlab do
args
end
- if Gitlab::EeCompatCheck.new(opts || {}).check
+ if File.basename(Rails.root) == 'gitlab-ee'
+ puts "Skipping EE projects"
+ exit 0
+ elsif Gitlab::EeCompatCheck.new(opts || {}).check
exit 0
else
exit 1
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 08677a98fc1..8377fe3269d 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -50,6 +50,8 @@ namespace :gitlab do
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
+ # For development and testing purposes, an extra storage is added to gitaly,
+ # which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_ruby: true)
storages = []
address = nil
@@ -67,6 +69,11 @@ namespace :gitlab do
storages << { name: key, path: val['path'] }
end
+
+ if Rails.env.test?
+ storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ end
+
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 42825f29e32..0e6aed32c52 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -79,7 +79,7 @@ namespace :gitlab do
if File.exist?(path_to_repo)
print '-'
else
- if Gitlab::Shell.new.add_repository(project.repository_storage_path,
+ if Gitlab::Shell.new.add_repository(project.repository_storage,
project.disk_path)
print '.'
else
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
new file mode 100644
index 00000000000..e05be4a3405
--- /dev/null
+++ b/lib/tasks/gitlab/storage.rake
@@ -0,0 +1,85 @@
+namespace :gitlab do
+ namespace :storage do
+ desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
+ task migrate_to_hashed: :environment do
+ legacy_projects_count = Project.with_legacy_storage.count
+
+ if legacy_projects_count == 0
+ puts 'There are no projects using legacy storage. Nothing to do!'
+
+ next
+ end
+
+ print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}"
+
+ project_id_batches do |start, finish|
+ StorageMigratorWorker.perform_async(start, finish)
+
+ print '.'
+ end
+
+ puts ' Done!'
+ end
+
+ desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage'
+ task legacy_projects: :environment do
+ projects_summary(Project.with_legacy_storage)
+ end
+
+ desc 'Gitlab | Storage | List existing projects using Legacy Storage'
+ task list_legacy_projects: :environment do
+ projects_list(Project.with_legacy_storage)
+ end
+
+ desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage'
+ task hashed_projects: :environment do
+ projects_summary(Project.with_hashed_storage)
+ end
+
+ desc 'Gitlab | Storage | List existing projects using Hashed Storage'
+ task list_hashed_projects: :environment do
+ projects_list(Project.with_hashed_storage)
+ end
+
+ def batch_size
+ ENV.fetch('BATCH', 200).to_i
+ end
+
+ def project_id_batches(&block)
+ Project.with_legacy_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
+ ids = relation.pluck(:id)
+
+ yield ids.min, ids.max
+ end
+ end
+
+ def projects_summary(relation)
+ projects_count = relation.count
+ puts "* Found #{projects_count} projects".color(:green)
+
+ projects_count
+ end
+
+ def projects_list(relation)
+ projects_count = projects_summary(relation)
+
+ projects = relation.with_route
+ limit = ENV.fetch('LIMIT', 500).to_i
+
+ return unless projects_count > 0
+
+ puts " ! Displaying first #{limit} projects..." if projects_count > limit
+
+ counter = 0
+ projects.find_in_batches(batch_size: batch_size) do |batch|
+ batch.each do |project|
+ counter += 1
+
+ puts " - #{project.full_path} (id: #{project.id})".color(:red)
+
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 4d485108cf6..7f86fd7b45e 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -39,13 +39,19 @@ class GithubImport
def import!
@project.force_import_start
+ import_success = false
+
timings = Benchmark.measure do
- Github::Import.new(@project, @options).execute
+ import_success = Github::Import.new(@project, @options).execute
end
- puts "Import finished. Timings: #{timings}".color(:green)
-
- @project.import_finish
+ if import_success
+ @project.import_finish
+ puts "Import finished. Timings: #{timings}".color(:green)
+ else
+ puts "Import was not successful. Errors were as follows:"
+ puts @project.import_error
+ end
end
def new_project
@@ -53,18 +59,23 @@ class GithubImport
namespace_path, _sep, name = @project_path.rpartition('/')
namespace = find_or_create_namespace(namespace_path)
- Projects::CreateService.new(
+ project = Projects::CreateService.new(
@current_user,
name: name,
path: name,
description: @repo['description'],
namespace_id: namespace.id,
visibility_level: visibility_level,
- import_type: 'github',
- import_source: @repo['full_name'],
- import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@"),
skip_wiki: @repo['has_wiki']
).execute
+
+ project.update!(
+ import_type: 'github',
+ import_source: @repo['full_name'],
+ import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@")
+ )
+
+ project
end
end
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index fcd4aa29834..38d63315fdc 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:45-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -29,6 +29,9 @@ msgstr[1] "%s Ð¿Ð¾Ð´Ð°Ð²Ð°Ð½Ð¸Ñ Ð±Ñха пропуÑнати, за да не Ñ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} подаде %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] "1 Ñхема"
msgstr[1] "%d Ñхеми"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Ðабор от графики отноÑно непрекъÑнатата интеграциÑ"
@@ -93,7 +99,7 @@ msgstr "ДобавÑне на нова папка"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -117,10 +123,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикачете файл чрез влачене и пуÑкане или %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,6 +207,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Клон"
@@ -194,6 +227,90 @@ msgstr "Превключване на клона"
msgid "Branches"
msgstr "Клонове"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Преглед на папката"
@@ -337,9 +454,6 @@ msgstr "Подадено от"
msgid "Compare"
msgstr "Сравнение"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "РъководÑтво за ÑътрудничеÑтво"
@@ -489,6 +603,9 @@ msgstr "Редактиране на плана %{id} за Ñхема"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -516,6 +633,9 @@ msgstr "Ð’Ñеки меÑец (на 1-во чиÑло, в 4 ч. Ñутринта
msgid "Every week (Sundays at 4:00am)"
msgstr "Ð’ÑÑка Ñедмица (в неделÑ, в 4 ч. Ñутринта)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "СобÑтвеникът не може да бъде променен"
@@ -572,7 +692,28 @@ msgstr "Към Вашето разклонение"
msgid "GoToYourFork|Fork"
msgstr "Разклонение"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr "Ðачало"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "ОÑвежаването започна уÑпешно"
@@ -620,6 +755,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Изключено"
@@ -684,6 +822,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -812,6 +953,18 @@ msgstr ""
msgid "Owner"
msgstr "СобÑтвеник"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -917,10 +1070,7 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -953,12 +1103,6 @@ msgstr "Връзката към изнеÑените данни на проекÑ
msgid "Project export started. A download link will be sent by email."
msgstr "ИзнаÑÑнето на проекта започна. Ще получите връзка към данните по е-поща."
-msgid "Project home"
-msgstr "Ðачална Ñтраница на проекта"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -983,6 +1127,27 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Графика"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Клонове"
msgid "RefSwitcher|Tags"
msgstr "Етикети"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Свързани подаваниÑ"
@@ -1055,6 +1223,9 @@ msgstr "Запазване на плана за Ñхема"
msgid "Schedule a new pipeline"
msgstr "Създаване на нов план за Ñхема"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Планиране на Ñхемите"
@@ -1094,6 +1265,12 @@ msgstr "зададете парола"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показване на %d Ñъбитие"
@@ -1102,6 +1279,102 @@ msgstr[1] "Показване на %d ÑъбитиÑ"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Изходен код"
@@ -1114,6 +1387,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "Звезда"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
@@ -1123,6 +1399,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "Преминаване към клон/етикет"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Етикет"
@@ -1188,6 +1467,9 @@ msgstr "СтойноÑтта, коÑто Ñе намира в Ñредата нÐ
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Това означава, че нÑма да можете да изпращате код, докато не Ñъздадете празно хранилище или не внеÑете ÑъщеÑтвуващо такова."
@@ -1363,9 +1645,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Използване на глобалната Ви наÑтройка за извеÑтиÑта"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Преглед на отворената заÑвка за Ñливане"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Вътрешен"
@@ -1438,6 +1726,12 @@ msgstr "ÐÑма да можете да изтеглÑте или изпраща
msgid "Your name"
msgstr "Вашето име"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "ден"
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 86deb620f0b..fc3c60166b7 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:45-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -18,8 +18,8 @@ msgstr ""
msgid "%d commit"
msgid_plural "%d commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d Commit"
+msgstr[1] "%d Commits"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -27,6 +27,9 @@ msgstr[0] "%s zusätzlicher Commit wurde ausgelassen um Leistungsprobleme zu ver
msgstr[1] "%s zusätzliche Commits wurden ausgelassen um Leistungsprobleme zu verhindern."
msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr "%{commit_author_link} hat %{commit_timeago} committet"
+
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
@@ -40,8 +43,8 @@ msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{storage_name}: fehlgeschlagener Speicherzugriff auf Host:"
+msgstr[1] "%{storage_name}: %{failed_attempts} fehlgeschlagene Speicherzugriffe:"
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(beachte die Informationen zur Installation auf %{link})."
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration"
@@ -67,13 +73,13 @@ msgid "Access to failing storages has been temporarily disabled to allow the mou
msgstr "Zugriff auf fehlerhafte Speicher wurde vorübergehend deaktiviert, um die Wiederherstellung zu ermöglichen. Für den zukünftigen Zugriff, behebe bitte das Problem und setze danach die Speicherinformationen zurück."
msgid "Account"
-msgstr ""
+msgstr "Konto"
msgid "Active"
-msgstr ""
+msgstr "Aktiv"
msgid "Activity"
-msgstr ""
+msgstr "Aktivität"
msgid "Add Changelog"
msgstr "Änderungsliste hinzufügen "
@@ -82,7 +88,7 @@ msgid "Add Contribution guide"
msgstr "Mitarbeitsanleitung hinzufügen"
msgid "Add License"
-msgstr ""
+msgstr "Lizenz hinzufügen"
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu übertragen (push) oder abzurufen (pull)."
@@ -93,11 +99,11 @@ msgstr "Erstelle eine neues Verzeichnis"
msgid "All"
msgstr "Alle"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "Anwendungen"
msgid "Archived project! Repository is read-only"
msgstr "Archiviertes Projekt! Repository ist nicht änderbar."
@@ -117,10 +123,34 @@ msgstr "Bist Du sicher, dass Du den Systemüberwachungstoken zurücksetzen wills
msgid "Are you sure?"
msgstr "Bist Du sicher?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Datei mittels Drag &amp; Drop oder %{upload_link} hinzufügen"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,10 +207,13 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Zweig"
+msgstr[1] "Zweige"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe Deine Änderungen. %{link_to_autodeploy_doc}"
@@ -194,6 +227,90 @@ msgstr "Branch wechseln"
msgid "Branches"
msgstr ""
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Verzeichnisse durchsuchen"
@@ -210,13 +327,13 @@ msgid "ByAuthor|by"
msgstr "von"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
-msgstr ""
+msgstr "CI-Konfiguration"
msgid "Cancel"
-msgstr ""
+msgstr "Abbrechen"
msgid "Cancel edit"
msgstr "Bearbeitung abbrechen"
@@ -240,7 +357,7 @@ msgid "Charts"
msgstr "Diagramme"
msgid "Chat"
-msgstr ""
+msgstr "Chat"
msgid "Cherry-pick this commit"
msgstr "Diesen Commit herauspicken "
@@ -307,8 +424,8 @@ msgstr "Kommentare"
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Commit"
+msgstr[1] "Commits"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Dauer der Commits in Minuten für die letzten 30 Commits"
@@ -323,7 +440,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "%{file_name} hinzufügen"
msgid "Commits"
-msgstr ""
+msgstr "Commits"
msgid "Commits feed"
msgstr "Liste der Commits"
@@ -337,9 +454,6 @@ msgstr "Committed von"
msgid "Compare"
msgstr "Vergleichen"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Mitarbeitsanleitung"
@@ -442,7 +556,7 @@ msgid "Description"
msgstr "Beschreibung"
msgid "Details"
-msgstr ""
+msgstr "Details"
msgid "Directory name"
msgstr "Verzeichnisname"
@@ -487,6 +601,9 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Pipeline Zeitplan bearbeiten %{id}"
msgid "Emails"
+msgstr "E-Mails"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
@@ -516,6 +633,9 @@ msgstr "Monatlich (am Ersten um 4:00 Uhr)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Wöchentlich (Sonntags um 4:00 Uhr)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Wechsel des Besitzers fehlgeschlagen"
@@ -572,7 +692,28 @@ msgstr "Gehe zu Deinem Ableger"
msgid "GoToYourFork|Fork"
msgstr "Ableger"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr "Keine Probleme erkannt"
msgid "HealthCheck|Unhealthy"
msgstr "Problematisch"
-msgid "Home"
-msgstr "Startseite"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "Aufräumen erfolgreich gestartet"
@@ -620,6 +755,9 @@ msgstr "Ticketereignisse"
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deaktiviert"
@@ -673,10 +811,10 @@ msgid "Locked Files"
msgstr ""
msgid "Median"
-msgstr ""
+msgstr "Median"
msgid "Members"
-msgstr ""
+msgstr "Mitglieder"
msgid "Merge Requests"
msgstr ""
@@ -684,14 +822,17 @@ msgstr ""
msgid "Merge events"
msgstr "Ereignisse zusammenführen"
-msgid "Messages"
+msgid "Merge request"
msgstr ""
+msgid "Messages"
+msgstr "Nachrichten"
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "einen SSH Schlüssel hinzufügst"
msgid "Monitoring"
-msgstr ""
+msgstr "Ãœberwachung"
msgid "More information is available|here"
msgstr "hier"
@@ -795,7 +936,7 @@ msgid "NotificationLevel|Watch"
msgstr "Beobachten"
msgid "Notifications"
-msgstr ""
+msgstr "Benachrichtigungen"
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
@@ -807,14 +948,26 @@ msgid "Options"
msgstr "Optionen"
msgid "Overview"
-msgstr ""
+msgstr "Ãœbersicht"
msgid "Owner"
msgstr "Besitzer"
-msgid "Password"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
msgstr ""
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr "Passwort"
+
msgid "Pipeline"
msgstr ""
@@ -900,7 +1053,7 @@ msgid "Pipelines for last week"
msgstr ""
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Pipelines des letzten Jahres"
msgid "Pipeline|all"
msgstr "Alle"
@@ -917,12 +1070,9 @@ msgstr "mit Stages"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
+msgid "Profile"
msgstr ""
-msgid "Project"
-msgstr "Projekt"
-
msgid "Project '%{project_name}' queued for deletion."
msgstr "Das Projekt '%{project_name}' wurde zur Löschung eingeplant."
@@ -953,12 +1103,6 @@ msgstr "Der Link für den Export des Projektes ist abgelaufen. Bitte generiere e
msgid "Project export started. A download link will be sent by email."
msgstr "Export des Projektes gestartet. Ein Link zum herunterladen wir Dir per E-Mail zugesandt."
-msgid "Project home"
-msgstr "Startseite des Projektes"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr "Abonnieren"
@@ -983,6 +1127,27 @@ msgstr "Stage"
msgid "ProjectNetworkGraph|Graph"
msgstr "Diagramm"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr "ProjectsDropdown | Projekte, die Sie häufig besuchen, werden hier angezeigt"
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Branches"
msgid "RefSwitcher|Tags"
msgstr "Tags"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Zugehörige Commits"
@@ -1047,7 +1215,7 @@ msgid "Revert this merge request"
msgstr "Merge Request zurücksetzen"
msgid "SSH Keys"
-msgstr ""
+msgstr "SSH-Schlüssel"
msgid "Save pipeline schedule"
msgstr "Zeitplan der Pipeline speichern"
@@ -1055,6 +1223,9 @@ msgstr "Zeitplan der Pipeline speichern"
msgid "Schedule a new pipeline"
msgstr "Plane eine neue Pipeline"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Pipelines planen"
@@ -1092,6 +1263,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "ein Passwort festlegst"
msgid "Settings"
+msgstr "Einstellungen"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1102,11 +1279,107 @@ msgstr[1] "Zeige %d Ereignisse"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Quellcode"
msgid "Spam Logs"
-msgstr ""
+msgstr "Spam-Protokolle"
msgid "Specify the following URL during the Runner setup:"
msgstr "Lege die folgende URL während des Runner Setups fest:"
@@ -1114,6 +1387,9 @@ msgstr "Lege die folgende URL während des Runner Setups fest:"
msgid "StarProject|Star"
msgstr "Favorisieren"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen"
@@ -1123,6 +1399,9 @@ msgstr "Starte den Runner!"
msgid "Switch branch/tag"
msgstr "Zu Branch/Tag wechseln"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] ""
@@ -1188,6 +1467,9 @@ msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Me
msgid "There are problems accessing Git storage: "
msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:"
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Dies bedeutet, dass Du keinen Code übertragen kannst, bevor Du kein leeres Repositorium erstellt oder ein Existierendes importiert hast."
@@ -1204,7 +1486,7 @@ msgid "Time until first merge request"
msgstr "Zeit bis zum ersten Merge Request"
msgid "Timeago|%s days ago"
-msgstr "seit %s Tagen"
+msgstr ""
msgid "Timeago|%s days remaining"
msgstr "%s Tage verbleibend"
@@ -1213,13 +1495,13 @@ msgid "Timeago|%s hours remaining"
msgstr "%s Stunden verbleibend"
msgid "Timeago|%s minutes ago"
-msgstr "seit %s Minuten "
+msgstr ""
msgid "Timeago|%s minutes remaining"
msgstr "%s Minuten verbleibend"
msgid "Timeago|%s months ago"
-msgstr "seit %s Monaten"
+msgstr ""
msgid "Timeago|%s months remaining"
msgstr "%s Monate verbleibend"
@@ -1228,13 +1510,13 @@ msgid "Timeago|%s seconds remaining"
msgstr "%s Sekunden verbleibend"
msgid "Timeago|%s weeks ago"
-msgstr "seit %s Wochen"
+msgstr ""
msgid "Timeago|%s weeks remaining"
msgstr "%s Wochen verbleibend"
msgid "Timeago|%s years ago"
-msgstr "seit %s Jahren"
+msgstr ""
msgid "Timeago|%s years remaining"
msgstr "%s Jahre verbleibend"
@@ -1363,9 +1645,15 @@ msgstr "Benutze den folgenden Registrierungstoken während des Setups:"
msgid "Use your global notification setting"
msgstr "Benutze Deine globalen Benachrichtigungseinstellungen"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Zeige offene Merge Requests."
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Intern"
@@ -1438,6 +1726,12 @@ msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachd
msgid "Your name"
msgstr "Dein Name"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "Tag"
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index 0ac591d4927..b50685514e1 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -1057,7 +1057,7 @@ msgstr ""
msgid "Timeago|a week ago"
msgstr ""
-msgid "Timeago|a while"
+msgid "Timeago|in a while"
msgstr ""
msgid "Timeago|a year ago"
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 8f25c893ecd..e8c2195e4e3 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:45-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -29,6 +29,9 @@ msgstr[1] "%s enmetadoj estis transsaltitaj, por ne troÅarÄi la sistemon."
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} enmetis %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] "1 ĉenstablo"
msgstr[1] "%d ĉenstabloj"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Aro da diagramoj pri la seninterrompa integrado"
@@ -93,7 +99,7 @@ msgstr "Aldoni novan dosierujon"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -117,10 +123,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Alkroĉu dosieron per Åovmetado aÅ­ %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,6 +207,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Branĉo"
@@ -194,6 +227,90 @@ msgstr "Iri al branĉo"
msgid "Branches"
msgstr "Branĉoj"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Foliumi dosierujon"
@@ -337,9 +454,6 @@ msgstr "Enmetita de"
msgid "Compare"
msgstr "Kompari"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Gvidlinioj por kontribuado"
@@ -489,6 +603,9 @@ msgstr "Redakti ĉenstablan planon %{id}"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -516,6 +633,9 @@ msgstr "Ĉiumonate (en la 1a de la monato, je 4:00)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Ĉiusemajne (en dimanĉo, je 4:00)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Ne eblas ÅanÄi la posedanton"
@@ -572,7 +692,28 @@ msgstr "Al via disbranĉigo"
msgid "GoToYourFork|Fork"
msgstr "Disbranĉigo"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr "Hejmo"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "La refreÅigo komenciÄis sukcese"
@@ -620,6 +755,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "MalÅaltita"
@@ -684,6 +822,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -812,6 +953,18 @@ msgstr ""
msgid "Owner"
msgstr "Posedanto"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -917,10 +1070,7 @@ msgstr "kun etapoj"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -953,12 +1103,6 @@ msgstr "La ligilo por la projekta elporto eksvalidiÄis. Bonvolu krei novan elpo
msgid "Project export started. A download link will be sent by email."
msgstr "La elporto de la projekto komenciÄis. Vi ricevos ligilon per retpoÅto por elÅuti la datenoj."
-msgid "Project home"
-msgstr "Hejmo de la projekto"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -983,6 +1127,27 @@ msgstr "Etapo"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafeo"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Branĉoj"
msgid "RefSwitcher|Tags"
msgstr "Etikedoj"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Rilataj enmetadoj"
@@ -1055,6 +1223,9 @@ msgstr "Konservi ĉenstablan planon"
msgid "Schedule a new pipeline"
msgstr "Plani novan ĉenstablon"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Planado de la ĉenstabloj"
@@ -1094,6 +1265,12 @@ msgstr "kreos pasvorton"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Estas montrata %d evento"
@@ -1102,6 +1279,102 @@ msgstr[1] "Estas montrataj %d eventoj"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Kodo"
@@ -1114,6 +1387,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "Steligi"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Kreu %{new_merge_request} kun ĉi tiuj ÅanÄoj"
@@ -1123,6 +1399,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "Iri al branĉo/etikedo"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Etikedo"
@@ -1188,6 +1467,9 @@ msgstr "La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemp
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Ĉi tiu signifas, ke vi ne povos alpuÅi kodon, antaÅ­ ol vi kreos malplenan deponejon aÅ­ enportos jam ekzistantan."
@@ -1363,9 +1645,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Uzi vian Äeneralan agordon pri la sciigoj"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Vidi la malfermitan peton pri kunfando"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Interna"
@@ -1438,6 +1726,12 @@ msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per SSH antaÅ­ ol vi %{add_ssh_key_
msgid "Your name"
msgstr "Via nomo"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "tago"
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index eee720d5ba2..29a010f9428 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:43-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -29,6 +29,9 @@ msgstr[1] "%s cambios adicionales han sido omitidos para evitar problemas de ren
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} cambió %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Una colección de gráficos sobre Integración Continua"
@@ -93,7 +99,7 @@ msgstr "Agregar nuevo directorio"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -117,10 +123,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,6 +207,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Rama"
@@ -194,6 +227,90 @@ msgstr "Cambiar rama"
msgid "Branches"
msgstr "Ramas"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Examinar directorio"
@@ -337,9 +454,6 @@ msgstr "Enviado por"
msgid "Compare"
msgstr "Comparar"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Guía de contribución"
@@ -489,6 +603,9 @@ msgstr "Editar Programación del Pipeline %{id}"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -516,6 +633,9 @@ msgstr "Todos los meses (el día 1 a las 4:00 am)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Todas las semanas (domingos a las 4:00 am)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Error al cambiar el propietario"
@@ -572,7 +692,28 @@ msgstr "Ir a tu bifurcación"
msgid "GoToYourFork|Fork"
msgstr "Bifurcación"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr "Inicio"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "Servicio de limpieza iniciado con éxito"
@@ -620,6 +755,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deshabilitado"
@@ -684,6 +822,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -812,6 +953,18 @@ msgstr ""
msgid "Owner"
msgstr "Propietario"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -917,10 +1070,7 @@ msgstr "con etapas"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -953,12 +1103,6 @@ msgstr "El enlace de exportación del proyecto ha caducado. Por favor, genera un
msgid "Project export started. A download link will be sent by email."
msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."
-msgid "Project home"
-msgstr "Inicio del proyecto"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -983,6 +1127,27 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Historial gráfico"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Ramas"
msgid "RefSwitcher|Tags"
msgstr "Etiquetas"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Cambios Relacionados"
@@ -1055,6 +1223,9 @@ msgstr "Guardar programación del pipeline"
msgid "Schedule a new pipeline"
msgstr "Programar un nuevo pipeline"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Programación de Pipelines"
@@ -1094,6 +1265,12 @@ msgstr "establecer una contraseña"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
@@ -1102,6 +1279,102 @@ msgstr[1] "Mostrando %d eventos"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Código fuente"
@@ -1114,6 +1387,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "Destacar"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Iniciar una %{new_merge_request} con estos cambios"
@@ -1123,6 +1399,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "Cambiar rama/etiqueta"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Etiqueta"
@@ -1188,6 +1467,9 @@ msgstr "El valor en el punto medio de una serie de valores observados. Por ejemp
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."
@@ -1363,9 +1645,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Utiliza tu configuración de notificación global"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Ver solicitud de fusión abierta"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -1438,6 +1726,12 @@ msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hast
msgid "Your name"
msgstr "Tu nombre"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "día"
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 43e66d8dea4..28d9c6a3e56 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:45-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -29,27 +29,33 @@ msgstr[1] "%s validations supplémentaires ont été masquées afin d'éviter de
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} a validé %{commit_timeago}"
-msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr "%{number_of_failures} sur %{maximum_failures} tentative(s). GitLab va vous permettre d'accéder à la prochaine tentative."
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} échecs sur %{maximum_failures}. GitLab va bloquer l’accès pendant %{number_of_seconds} secondes."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} échecs sur %{maximum_failures}. GitLab ne va plus réessayer automatiquement. Réinitialisez les informations de stockage lorsque le problème est résolu."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{storage_name} : la tentative d’accès au stockage a échouée sur l’hôte :"
+msgstr[1] "%{storage_name} : %{failed_attempts} tentatives d’accès au stockage ont échouées :"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(Lisez %{link} pour savoir comment l'installer)."
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 pipeline"
+msgstr[1] "%d pipelines"
+
+msgid "1st contribution!"
+msgstr ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
@@ -58,16 +64,16 @@ msgid "About auto deploy"
msgstr "A propos de l'auto-déploiement"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Rapports d’abus"
msgid "Access Tokens"
-msgstr ""
+msgstr "Jetons d'Accès"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr ""
+msgstr "L'accès aux stockages défaillants a été temporairement désactivé pour permettre au montage de récupérer. Réinitialiser les informations de stockage dès que le problème est résolu pour permettre l’accès à nouveau."
msgid "Account"
-msgstr ""
+msgstr "Compte"
msgid "Active"
msgstr "Actif"
@@ -91,13 +97,13 @@ msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
msgid "All"
-msgstr ""
+msgstr "Tous"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "Applications"
msgid "Archived project! Repository is read-only"
msgstr "Projet archivé ! Le dépôt est en lecture seule"
@@ -106,21 +112,45 @@ msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Êtes-vous sûr de vouloir supprimer ce pipeline programmé"
msgid "Are you sure you want to discard your changes?"
-msgstr ""
+msgstr "Êtes-vous sûr de vouloir annuler vos modifications ?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton d’inscription ?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton de bilan de santé ?"
msgid "Are you sure?"
+msgstr "Êtes-vous certain ?"
+
+msgid "Artifacts"
msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Attachez un fichier par glisser &amp; déposer ou %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,10 +207,13 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Branche"
+msgstr[1] "Branches"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier Yaml pour l'intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
@@ -192,6 +225,90 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Changer de branche"
msgid "Branches"
+msgstr "Branches"
+
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
msgstr ""
msgid "Browse Directory"
@@ -210,7 +327,7 @@ msgid "ByAuthor|by"
msgstr "par"
msgid "CI / CD"
-msgstr ""
+msgstr "Intégration continu / Déploiement continu"
msgid "CI configuration"
msgstr "Configuration de l'intégration continue (CI)"
@@ -219,19 +336,19 @@ msgid "Cancel"
msgstr "Annuler"
msgid "Cancel edit"
-msgstr ""
+msgstr "Annuler modification"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Sélectionner dans la branche"
msgid "ChangeTypeActionLabel|Revert in branch"
-msgstr "Annuler dans la branche"
+msgstr "Défaire dans la branche"
msgid "ChangeTypeAction|Cherry-pick"
msgstr "Sélectionner"
msgid "ChangeTypeAction|Revert"
-msgstr "Annuler"
+msgstr "Défaire"
msgid "Changelog"
msgstr "Journal des modifications"
@@ -240,7 +357,7 @@ msgid "Charts"
msgstr "Graphiques"
msgid "Chat"
-msgstr ""
+msgstr "Chat"
msgid "Cherry-pick this commit"
msgstr "Sélectionner cette validation"
@@ -261,10 +378,10 @@ msgid "CiStatusLabel|manual action"
msgstr "action manuelle"
msgid "CiStatusLabel|passed"
-msgstr "passé"
+msgstr "réussi"
msgid "CiStatusLabel|passed with warnings"
-msgstr "passé avec des avertissements"
+msgstr "réussi avec des avertissements"
msgid "CiStatusLabel|pending"
msgstr "en attente"
@@ -279,7 +396,7 @@ msgid "CiStatusText|blocked"
msgstr "bloqué"
msgid "CiStatusText|canceled"
-msgstr "annulé "
+msgstr "annulé"
msgid "CiStatusText|created"
msgstr "créé"
@@ -291,7 +408,7 @@ msgid "CiStatusText|manual"
msgstr "manuel"
msgid "CiStatusText|passed"
-msgstr "passé"
+msgstr "réussi"
msgid "CiStatusText|pending"
msgstr "en attente"
@@ -303,7 +420,7 @@ msgid "CiStatus|running"
msgstr "en cours"
msgid "Comments"
-msgstr ""
+msgstr "Commentaires"
msgid "Commit"
msgid_plural "Commits"
@@ -337,9 +454,6 @@ msgstr "Validé par"
msgid "Compare"
msgstr "Comparer"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Guilde de contribution"
@@ -359,7 +473,7 @@ msgid "Create New Directory"
msgstr "Créer un nouveau dossier"
msgid "Create a new branch"
-msgstr ""
+msgstr "Créer une nouvelle branche"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Créer un jeton d’accès personnel pour votre compte afin de récupérer ou pousser par %{protocol}."
@@ -436,19 +550,19 @@ msgstr[0] "Déploiement"
msgstr[1] "Déploiements"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Clés de déploiement"
msgid "Description"
-msgstr ""
+msgstr "Description"
msgid "Details"
-msgstr ""
+msgstr "Détails"
msgid "Directory name"
msgstr "Nom du dossier"
msgid "Discard changes"
-msgstr ""
+msgstr "Supprimer les modifications"
msgid "Don't show again"
msgstr "Ne plus montrer"
@@ -487,25 +601,28 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Éditer le pipeline programmé %{id}"
msgid "Emails"
+msgstr "Courriels"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
-msgstr ""
+msgstr "Aucun filtre"
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "Filtrer par commentaires"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "Filtrer par événements d'incident"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "Filtrer par événements de fusion"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "Filtrer par événements de poussée"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "Filtrer par équipe"
msgid "Every day (at 4:00am)"
msgstr "Chaque jour (à 4:00 du matin)"
@@ -516,6 +633,9 @@ msgstr "Chaque mois (le 1er à 4:00 du matin)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Chaque semaine (dimanche à 4:00 du matin)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Échec du changement de propriétaire"
@@ -555,16 +675,16 @@ msgid "From merge request merge until deploy to production"
msgstr "Depuis la fusion de la demande de fusion jusqu'au déploiement en production"
msgid "GPG Keys"
-msgstr ""
+msgstr "Clés GPG"
msgid "Geo Nodes"
msgstr ""
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Les informations de santé du stockage Git ont été réinitialisées"
msgid "GitLab Runner section"
-msgstr ""
+msgstr "Section de Runner GitLab"
msgid "Go to your fork"
msgstr "Aller à votre fourche"
@@ -572,33 +692,48 @@ msgstr "Aller à votre fourche"
msgid "GoToYourFork|Fork"
msgstr "Fourche"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
-msgid "Health Check"
+msgid "GroupSettings|Share with group lock"
msgstr ""
-msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
-msgid "HealthCheck|Access token is"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|Healthy"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|No Health Problems Detected"
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
msgstr ""
-msgid "HealthCheck|Unhealthy"
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
-msgid "Home"
-msgstr "Accueil"
-
-msgid "Hooks"
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Health Check"
+msgstr "Bilan de santé"
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr "Des informations de santé peuvent être récupérées depuis les adresses suivantes. Plus d’informations"
+
+msgid "HealthCheck|Access token is"
+msgstr "Le jeton d’accès est"
+
+msgid "HealthCheck|Healthy"
+msgstr "En bonne santé"
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr "Aucun problème détecté"
+
+msgid "HealthCheck|Unhealthy"
+msgstr "En mauvaise santé"
+
msgid "Housekeeping successfully started"
msgstr "Maintenance démarrée avec succès"
@@ -606,7 +741,7 @@ msgid "Import repository"
msgstr "Importer un dépôt"
msgid "Install a Runner compatible with GitLab CI"
-msgstr ""
+msgstr "Installez un Runner compatible avec l'intégration continue de GitLab"
msgid "Interval Pattern"
msgstr "Schéma d’intervalle"
@@ -615,9 +750,12 @@ msgid "Introducing Cycle Analytics"
msgstr "Introduction à l'analyseur de cycle"
msgid "Issue events"
-msgstr ""
+msgstr "Événements de l'incident"
msgid "Issues"
+msgstr "Incidents"
+
+msgid "Jobs"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -644,10 +782,10 @@ msgid "Last commit"
msgstr "Dernière validation"
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "Vous avez poussé sur"
msgid "LastPushEvent|at"
-msgstr ""
+msgstr "à"
msgid "Learn more in the"
msgstr "En apprendre plus dans le"
@@ -676,25 +814,28 @@ msgid "Median"
msgstr "Médian"
msgid "Members"
-msgstr ""
+msgstr "Membres"
msgid "Merge Requests"
-msgstr ""
+msgstr "Demandes de fusion"
msgid "Merge events"
+msgstr "Événements de fusion"
+
+msgid "Merge request"
msgstr ""
msgid "Messages"
-msgstr ""
+msgstr "Messages"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "ajouter une clef SSH"
msgid "Monitoring"
-msgstr ""
+msgstr "Surveillance"
msgid "More information is available|here"
-msgstr ""
+msgstr "ici"
msgid "New Issue"
msgid_plural "New Issues"
@@ -795,7 +936,7 @@ msgid "NotificationLevel|Watch"
msgstr "Surveillé"
msgid "Notifications"
-msgstr ""
+msgstr "Notifications"
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
@@ -804,20 +945,32 @@ msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
msgid "Options"
-msgstr ""
+msgstr "Paramètres"
msgid "Overview"
-msgstr ""
+msgstr "Vue d'ensemble"
msgid "Owner"
msgstr "Propriétaire"
-msgid "Password"
+msgid "Pagination|Last »"
msgstr ""
-msgid "Pipeline"
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
msgstr ""
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr "Mot de Passe"
+
+msgid "Pipeline"
+msgstr "Pipeline"
+
msgid "Pipeline Health"
msgstr "Santé du Pipeline"
@@ -888,19 +1041,19 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personnalisé"
msgid "Pipelines"
-msgstr ""
+msgstr "Pipelines"
msgid "Pipelines charts"
msgstr "Graphique des pipelines"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Pipelines pour le dernier mois"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Pipelines pour la dernière semaine"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Pipelines pour la dernière année"
msgid "Pipeline|all"
msgstr "Tous"
@@ -915,12 +1068,9 @@ msgid "Pipeline|with stages"
msgstr "avec les étapes"
msgid "Preferences"
-msgstr ""
-
-msgid "Profile Settings"
-msgstr ""
+msgstr "Préférences"
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -939,7 +1089,7 @@ msgid "Project access must be granted explicitly to each user."
msgstr "L’accès au projet doit être explicitement accordé à chaque utilisateur."
msgid "Project details"
-msgstr ""
+msgstr "Détails du projet"
msgid "Project export could not be deleted."
msgstr "L'export du projet n'a pas pu être supprimé."
@@ -953,14 +1103,8 @@ msgstr "Le lien de l’export du projet a expiré. Merci de générer un nouvel
msgid "Project export started. A download link will be sent by email."
msgstr "L'export du projet a débuté. Un lien de téléchargement sera envoyé par courriel."
-msgid "Project home"
-msgstr "Accueil du projet"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "S’abonner"
msgid "ProjectFeature|Disabled"
msgstr "Désactivé"
@@ -983,11 +1127,32 @@ msgstr "Étape"
msgid "ProjectNetworkGraph|Graph"
msgstr "Graphique "
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr "Chargement des projets"
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr "Les projets que vous visitez souvent apparaîtront ici"
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr "Chercher dans vos projets"
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr "Désolé, aucun projet ne correspond à votre recherche"
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr "Cette fonctionnalité requiert le support du localStorage par votre navigateur"
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
-msgstr ""
+msgstr "Évènements de poussée"
msgid "Read more"
msgstr "Lire plus"
@@ -1001,6 +1166,9 @@ msgstr "Branches"
msgid "RefSwitcher|Tags"
msgstr "Étiquettes"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Validations liés"
@@ -1026,19 +1194,19 @@ msgid "Remove project"
msgstr "Supprimer le projet"
msgid "Repository"
-msgstr ""
+msgstr "Dépôt"
msgid "Request Access"
msgstr "Demander l'accès"
msgid "Reset git storage health information"
-msgstr ""
+msgstr "Réinitialiser les informations de santé du stockage Git"
msgid "Reset health check access token"
-msgstr ""
+msgstr "Réinitialiser le jeton d’accès au bilan de santé"
msgid "Reset runners registration token"
-msgstr ""
+msgstr "Réinitialiser le jeton d’inscription des Runners"
msgid "Revert this commit"
msgstr "Annuler cette validation"
@@ -1047,7 +1215,7 @@ msgid "Revert this merge request"
msgstr "Annuler cette demande de fusion"
msgid "SSH Keys"
-msgstr ""
+msgstr "Clés SSH"
msgid "Save pipeline schedule"
msgstr "Sauvegarder le pipeline programmé"
@@ -1055,6 +1223,9 @@ msgstr "Sauvegarder le pipeline programmé"
msgid "Schedule a new pipeline"
msgstr "Programmer un nouveau pipeline"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Programmer des pipelines"
@@ -1068,13 +1239,13 @@ msgid "Select a timezone"
msgstr "Sélectionnez un fuseau horaire"
msgid "Select existing branch"
-msgstr ""
+msgstr "Sélectionnez une branche existante"
msgid "Select target branch"
msgstr "Sélectionnez une branche cible"
msgid "Service Templates"
-msgstr ""
+msgstr "Modèles de service"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par %{protocol}."
@@ -1092,6 +1263,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "définir un mot de passe"
msgid "Settings"
+msgstr "Paramètres"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1100,29 +1277,131 @@ msgstr[0] "Affichage de %d évènement"
msgstr[1] "Affichage de %d évènements"
msgid "Snippets"
+msgstr "Extraits de code"
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
msgstr ""
msgid "Source code"
msgstr "Code source"
msgid "Spam Logs"
-msgstr ""
+msgstr "Journaux des messages indésirables"
msgid "Specify the following URL during the Runner setup:"
-msgstr ""
+msgstr "Spécifiez l’URL suivante lors de la configuration du Runner :"
msgid "StarProject|Star"
msgstr "S'abonner"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Créer une %{new_merge_request} avec ces changements"
msgid "Start the Runner!"
-msgstr ""
+msgstr "Démarrer le Runner !"
msgid "Switch branch/tag"
msgstr "Changer de branche / d'étiquette"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Étiquette"
@@ -1135,7 +1414,7 @@ msgid "Target Branch"
msgstr "Branche cible"
msgid "Team"
-msgstr ""
+msgstr "Équipe"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion."
@@ -1186,6 +1465,9 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "La valeur située au point médian d’une série de valeur observée. C.à.d., entre 3, 5, 9, le médian est 5. Entre 3, 5, 7, 8, le médian est (5+7)/2 = 6."
msgid "There are problems accessing Git storage: "
+msgstr "Il y a des difficultés à accéder aux données Git : "
+
+msgid "This is the author's first Merge Request to this project. Handle with care."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
@@ -1358,14 +1640,20 @@ msgid "UploadLink|click to upload"
msgstr "Cliquez pour envoyer"
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "Utiliser le jeton d’inscription suivant pendant l’installation :"
msgid "Use your global notification setting"
msgstr "Utiliser vos paramètres de notification globaux"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Afficher la demande de fusion"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Interne"
@@ -1385,7 +1673,7 @@ msgid "We don't have enough data to show this stage."
msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape."
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "Retirer la demande d'accès"
@@ -1438,6 +1726,12 @@ msgstr "Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous
msgid "Your name"
msgstr "Votre nom"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "jour"
@@ -1451,6 +1745,6 @@ msgstr "courriels de notification"
msgid "parent"
msgid_plural "parents"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "parent"
+msgstr[1] "parents"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bd3d5b7c839..c73e582e608 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3,12 +3,13 @@
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
+#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 08:32+0200\n"
+"POT-Creation-Date: 2017-10-06 18:33+0200\n"
+"PO-Revision-Date: 2017-10-06 18:33+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -22,6 +23,11 @@ msgid_plural "%d commits"
msgstr[0] ""
msgstr[1] ""
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
@@ -30,6 +36,9 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -52,6 +61,12 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1st contribution!"
+msgstr ""
+
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -94,7 +109,7 @@ msgstr ""
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -118,81 +133,132 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
-msgid "Authentication log"
+msgid "Authentication Log"
msgstr ""
-msgid "Billing"
+msgid "Author"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "AutoDevOps|Enable in settings"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Branch"
+msgid_plural "Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "BranchSwitcherTitle|Switch branch"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Branches"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Branches|Cant find HEAD commit for this branch"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Branches|Compare"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Branches|Delete branch"
msgstr ""
-msgid "Billinglans|Downgrade"
+msgid "Branches|Delete merged branches"
msgstr ""
-msgid "Branch"
-msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
+msgid "Branches|Delete protected branch"
+msgstr ""
-msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
+msgid "Branches|Delete protected branch '%{branch_name}'?"
msgstr ""
-msgid "BranchSwitcherPlaceholder|Search branches"
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
msgstr ""
-msgid "BranchSwitcherTitle|Switch branch"
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
msgstr ""
-msgid "Branches"
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
msgstr ""
msgid "Browse Directory"
@@ -216,6 +282,9 @@ msgstr ""
msgid "CI configuration"
msgstr ""
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -303,6 +372,132 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "Clone repository"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -311,6 +506,9 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
@@ -341,13 +539,52 @@ msgstr ""
msgid "Container Registry"
msgstr ""
-msgid "Contribution guide"
+msgid "ContainerRegistry|Created"
msgstr ""
-msgid "Contributors"
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
msgstr ""
msgid "Copy URL to clipboard"
@@ -359,9 +596,6 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
-msgid "Create a new branch"
-msgstr ""
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
@@ -425,6 +659,12 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -517,6 +757,9 @@ msgstr ""
msgid "Every week (Sundays at 4:00am)"
msgstr ""
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
@@ -549,6 +792,12 @@ msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr ""
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr ""
@@ -558,9 +807,6 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Geo Nodes"
-msgstr ""
-
msgid "Git storage health information has been reset"
msgstr ""
@@ -573,7 +819,31 @@ msgstr ""
msgid "GoToYourFork|Fork"
msgstr ""
-msgid "Group overview"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -594,10 +864,7 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr ""
-
-msgid "Hooks"
+msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
@@ -618,6 +885,9 @@ msgstr ""
msgid "Issue events"
msgstr ""
+msgid "IssueBoards|Board"
+msgstr ""
+
msgid "Issues"
msgstr ""
@@ -638,10 +908,19 @@ msgstr[1] ""
msgid "Last Pipeline"
msgstr ""
-msgid "Last Update"
+msgid "Last commit"
msgstr ""
-msgid "Last commit"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
msgstr ""
msgid "LastPushEvent|You pushed to"
@@ -662,17 +941,11 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
-msgid "Locked Files"
-msgstr ""
-
msgid "Median"
msgstr ""
@@ -685,6 +958,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -729,6 +1005,9 @@ msgstr ""
msgid "New tag"
msgstr ""
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr ""
@@ -804,6 +1083,9 @@ msgstr ""
msgid "OpenedNDaysAgo|Opened"
msgstr ""
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr ""
@@ -813,6 +1095,18 @@ msgstr ""
msgid "Owner"
msgstr ""
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -828,9 +1122,6 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -918,10 +1209,7 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -954,12 +1242,6 @@ msgstr ""
msgid "Project export started. A download link will be sent by email."
msgstr ""
-msgid "Project home"
-msgstr ""
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -984,22 +1266,22 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
-msgid "Push Rules"
+msgid "ProjectsDropdown|Frequently visited"
msgstr ""
msgid "ProjectsDropdown|Loading projects"
msgstr ""
-msgid "ProjectsDropdown|Sorry, no projects matched your search"
-msgstr ""
-
msgid "ProjectsDropdown|Projects you visit often will appear here"
msgstr ""
msgid "ProjectsDropdown|Search your projects"
msgstr ""
-msgid "ProjectsDropdown|Something went wrong on our end"
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
@@ -1068,12 +1350,18 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr ""
msgid "Schedule a new pipeline"
msgstr ""
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr ""
@@ -1086,9 +1374,6 @@ msgstr ""
msgid "Select a timezone"
msgstr ""
-msgid "Select existing branch"
-msgstr ""
-
msgid "Select target branch"
msgstr ""
@@ -1113,6 +1398,12 @@ msgstr ""
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -1121,6 +1412,102 @@ msgstr[1] ""
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
msgid "Source code"
msgstr ""
@@ -1133,6 +1520,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr ""
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
@@ -1142,6 +1532,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr ""
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] ""
@@ -1207,6 +1600,9 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -1288,9 +1684,6 @@ msgstr ""
msgid "Timeago|a week ago"
msgstr ""
-msgid "Timeago|a while"
-msgstr ""
-
msgid "Timeago|a year ago"
msgstr ""
@@ -1342,6 +1735,9 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr ""
@@ -1382,9 +1778,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr ""
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr ""
@@ -1406,6 +1808,105 @@ msgstr ""
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr ""
@@ -1457,6 +1958,9 @@ msgstr ""
msgid "Your name"
msgstr ""
+msgid "Your projects"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] ""
@@ -1472,3 +1976,6 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
+
+msgid "personal access token"
+msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 46b3e12f97c..804817e96e9 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:44-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -29,6 +29,9 @@ msgstr[1] "%s commit aggiuntivi sono stati omessi per evitare degradi di prestaz
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} ha committato %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un insieme di grafici riguardo la Continuous Integration"
@@ -93,7 +99,7 @@ msgstr "Aggiungi una directory (cartella)"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -117,10 +123,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,6 +207,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -194,6 +227,90 @@ msgstr "Cambia branch"
msgid "Branches"
msgstr ""
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Naviga direttori"
@@ -337,9 +454,6 @@ msgstr "Committato da "
msgid "Compare"
msgstr "Confronta"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Guida per contribuire"
@@ -489,6 +603,9 @@ msgstr "Cambia programmazione della pipeline %{id}"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -516,6 +633,9 @@ msgstr "Ogni primo giorno del mese (alle 4 del mattino)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Ogni settimana (Di domenica alle 4 del mattino)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Impossibile cambiare owner"
@@ -572,7 +692,28 @@ msgstr "Vai il tuo fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr ""
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
@@ -620,6 +755,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Disabilitato"
@@ -684,6 +822,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -812,6 +953,18 @@ msgstr ""
msgid "Owner"
msgstr ""
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -917,10 +1070,7 @@ msgstr "con più stadi"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -953,12 +1103,6 @@ msgstr "Il link d'esportazione del progetto è scaduto. Genera una nuova esporta
msgid "Project export started. A download link will be sent by email."
msgstr "Esportazione del progetto iniziata. Un link di download sarà inviato via email."
-msgid "Project home"
-msgstr "Home di progetto"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -983,6 +1127,27 @@ msgstr "Stadio"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafico"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Branches"
msgid "RefSwitcher|Tags"
msgstr "Tags"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Commit correlati"
@@ -1055,6 +1223,9 @@ msgstr "Salva pianificazione pipeline"
msgid "Schedule a new pipeline"
msgstr "Pianifica una nuova Pipeline"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Pianificazione pipelines"
@@ -1094,6 +1265,12 @@ msgstr "imposta una password"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Visualizza %d evento"
@@ -1102,6 +1279,102 @@ msgstr[1] "Visualizza %d eventi"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Codice Sorgente"
@@ -1114,6 +1387,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "Star"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "inizia una %{new_merge_request} con queste modifiche"
@@ -1123,6 +1399,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "Cambia branch/tag"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] ""
@@ -1188,6 +1467,9 @@ msgstr "Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Questo significa che non è possibile effettuare push di codice fino a che non crei una repository vuota o ne importi una esistente"
@@ -1363,9 +1645,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Usa le tue impostazioni globali "
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Mostra la richieste di merge aperte"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -1438,6 +1726,12 @@ msgstr "Non sarai in grado di effettuare push o pull tramite SSH fino a che %{ad
msgid "Your name"
msgstr "Il tuo nome"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "giorno"
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index bc25b69c80a..2a08abda7ce 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:44-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -27,6 +27,9 @@ msgstr[0] "パフォーマンス低下をé¿ã‘ã‚‹ãŸã‚ %s 個ã®ã‚³ãƒŸãƒƒãƒˆã‚
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago}ã«%{commit_author_link}ãŒã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸã€‚"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -47,6 +50,9 @@ msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 個ã®ãƒ‘イプライン"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
@@ -89,7 +95,7 @@ msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -113,10 +119,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "ドラッグ&ドロップã¾ãŸã¯ %{upload_link} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -173,6 +203,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "ブランãƒ"
@@ -189,6 +222,90 @@ msgstr "ブランãƒã‚’切替"
msgid "Branches"
msgstr "ブランãƒ"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "ディレクトリを表示"
@@ -331,9 +448,6 @@ msgstr "コミット担当者: "
msgid "Compare"
msgstr "比較"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "貢献者å‘ã‘ガイド"
@@ -482,6 +596,9 @@ msgstr "パイプラインスケジュール %{id} を編集"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -509,6 +626,9 @@ msgstr "毎月 (1æ—¥ã®åˆå‰4:00)"
msgid "Every week (Sundays at 4:00am)"
msgstr "毎週 (日曜日ã®åˆå‰4:00)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "オーナーを変更ã§ãã¾ã›ã‚“ã§ã—ãŸ"
@@ -564,7 +684,28 @@ msgstr "自分ã®ãƒ•ã‚©ãƒ¼ã‚¯ã¸ç§»å‹•"
msgid "GoToYourFork|Fork"
msgstr "フォーク"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -585,12 +726,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr "ホーム"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ングã¯æ­£å¸¸ã«èµ·å‹•ã—ã¾ã—ãŸã€‚"
@@ -612,6 +747,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "無効"
@@ -674,6 +812,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -801,6 +942,18 @@ msgstr ""
msgid "Owner"
msgstr "オーナー"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -906,10 +1059,7 @@ msgstr "ステージã‚ã‚Š"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -942,12 +1092,6 @@ msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆãƒªãƒ³ã‚¯ã¯æœŸé™åˆ‡ã‚Œã«ãªã‚Š
msgid "Project export started. A download link will be sent by email."
msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’開始ã—ã¾ã—ãŸã€‚ダウンロードã®ãƒªãƒ³ã‚¯ã¯ãƒ¡ãƒ¼ãƒ«ã§é€ä¿¡ã—ã¾ã™"
-msgid "Project home"
-msgstr "プロジェクトホーム"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -972,6 +1116,27 @@ msgstr "ステージ"
msgid "ProjectNetworkGraph|Graph"
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -990,6 +1155,9 @@ msgstr "ブランãƒ"
msgid "RefSwitcher|Tags"
msgstr "ã‚¿ã‚°"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "関連ã™ã‚‹ã‚³ãƒŸãƒƒãƒˆ"
@@ -1044,6 +1212,9 @@ msgstr "パイプラインスケジュールをä¿å­˜"
msgid "Schedule a new pipeline"
msgstr "æ–°ã—ã„パイプラインã®ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã‚’作æˆ"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "パイプラインスケジューリング"
@@ -1083,6 +1254,12 @@ msgstr "パスワードを設定"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
@@ -1090,6 +1267,102 @@ msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "ソースコード"
@@ -1102,6 +1375,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "スターを付ã‘ã‚‹"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
@@ -1111,6 +1387,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "ã‚¿ã‚°"
@@ -1175,6 +1454,9 @@ msgstr "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "空レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆã¾ãŸã¯æ—¢å­˜ãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã‚’インãƒãƒ¼ãƒˆã‚’ã—ãªã‘ã‚Œã°ã€ã‚³ãƒ¼ãƒ‰ã®ãƒ—ッシュã¯ã§ãã¾ã›ã‚“。"
@@ -1348,9 +1630,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "全体通知設定を利用"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "オープンãªãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’表示"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "内部"
@@ -1423,6 +1711,12 @@ msgstr "%{add_ssh_key_link} をプロファイルã«è¿½åŠ ã—ã¦ã„ãªã„ã®ã§ã
msgid "Your name"
msgstr "åå‰"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "æ—¥"
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 4baefdb9a3e..de4a13d3765 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:43-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -27,6 +27,9 @@ msgstr[0] "%s 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago} ì— %{commit_author_link} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. "
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "%{number_of_failures} / %{maximum_failures} 실패. GitLab ì€ ë‹¤ìŒ ì‹œë„ì—ì„œ 성공하면 ì ‘ê·¼ì„ í—ˆìš©í•  것입니다."
@@ -47,6 +50,9 @@ msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 파ì´í”„ë¼ì¸"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
@@ -89,7 +95,7 @@ msgstr "새 디렉토리 추가"
msgid "All"
msgstr "ì „ì²´"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -113,10 +119,34 @@ msgstr "헬스 ì²´í¬ í† í°ì„ 초기화 하시겠습니까?"
msgid "Are you sure?"
msgstr "확실합니까?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "드래그 &amp; 드롭 ë˜ëŠ” %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -173,6 +203,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "브랜치"
@@ -189,6 +222,90 @@ msgstr "브랜치 변경"
msgid "Branches"
msgstr "브랜치"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "디렉토리 찾아보기"
@@ -331,9 +448,6 @@ msgstr "커밋한 사용ìž"
msgid "Compare"
msgstr "비êµ"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내"
@@ -482,6 +596,9 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "모든 ê°’ì„ ê¸°ì¤€ìœ¼ë¡œ í•„í„°"
@@ -509,6 +626,9 @@ msgstr "매월 (1ì¼ ì˜¤ì „ 4ì‹œ)"
msgid "Every week (Sundays at 4:00am)"
msgstr "매주 (ì¼ìš”ì¼ ì˜¤ì „ 4ì‹œì—)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "소유ìžë¥¼ 변경하지 못했습니다"
@@ -564,7 +684,28 @@ msgstr "ë‹¹ì‹ ì˜ í¬í¬ë¡œ ì´ë™í•˜ì„¸ìš”"
msgid "GoToYourFork|Fork"
msgstr "í¬í¬"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -585,12 +726,6 @@ msgstr " 헬스 문제가 발견ë˜ì§€ 않았습니다."
msgid "HealthCheck|Unhealthy"
msgstr "비정ìƒ"
-msgid "Home"
-msgstr "홈"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "Housekeepingì´ ì„±ê³µì ìœ¼ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤"
@@ -612,6 +747,9 @@ msgstr "ì´ìŠˆ ì´ë²¤íŠ¸"
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Disabled"
@@ -674,6 +812,9 @@ msgstr ""
msgid "Merge events"
msgstr "머지 ì´ë²¤íŠ¸"
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -801,6 +942,18 @@ msgstr ""
msgid "Owner"
msgstr "소유ìž"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -906,10 +1059,7 @@ msgstr "스테ì´ì§•"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -942,12 +1092,6 @@ msgstr "프로ì íŠ¸ 내보내기 ë§í¬ê°€ 만료ë˜ì—ˆìŠµë‹ˆë‹¤. 프로ì íŠ¸
msgid "Project export started. A download link will be sent by email."
msgstr "프로ì íŠ¸ 내보내기가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. 다운로드 ë§í¬ëŠ” ì´ë©”ì¼ë¡œ 전송ë©ë‹ˆë‹¤."
-msgid "Project home"
-msgstr "프로ì íŠ¸ 홈"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr "구ë…"
@@ -972,6 +1116,27 @@ msgstr "스테ì´ì§•"
msgid "ProjectNetworkGraph|Graph"
msgstr "그래프"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -990,6 +1155,9 @@ msgstr "브랜치"
msgid "RefSwitcher|Tags"
msgstr "태그"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "관련 커밋"
@@ -1044,6 +1212,9 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄 저장"
msgid "Schedule a new pipeline"
msgstr "새로운 파ì´í”„ë¼ì¸ 스케줄 잡기"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "파ì´í”„ë¼ì¸ 스케줄ë§"
@@ -1083,6 +1254,12 @@ msgstr "패스워드 설정"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
@@ -1090,6 +1267,102 @@ msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "소스 코드"
@@ -1102,6 +1375,9 @@ msgstr "Runner 설정 중 ë‹¤ìŒ URLì„ ì§€ì •í•˜ì„¸ìš”."
msgid "StarProject|Star"
msgstr "별표"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
@@ -1111,6 +1387,9 @@ msgstr "Runner 시작!"
msgid "Switch branch/tag"
msgstr "스위치 브랜치/태그"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "태그"
@@ -1175,6 +1454,9 @@ msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5,
msgid "There are problems accessing Git storage: "
msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까지 코드를 Push 할 수 없습니다."
@@ -1348,9 +1630,15 @@ msgstr "설정 ì¤‘ì— ë‹¤ìŒ ë“±ë¡ í† í° ì´ìš© : "
msgid "Use your global notification setting"
msgstr "전체 알림 설정 사용"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "열린 머지 리퀘스트보기"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "내부"
@@ -1423,6 +1711,12 @@ msgstr "ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} 를 하기 ì „ì—는 SSH를 í
msgid "Your name"
msgstr "ê·€í•˜ì˜ ì´ë¦„"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "ì¼"
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
new file mode 100644
index 00000000000..45a444fac43
--- /dev/null
+++ b/locale/nl_NL/gitlab.po
@@ -0,0 +1,1750 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Dutch\n"
+"Language: nl_NL\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: nl\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d commit"
+msgstr[1] "%d commits"
+
+msgid "%s additional commit has been omitted to prevent performance issues."
+msgid_plural "%s additional commits have been omitted to prevent performance issues."
+msgstr[0] "%s andere commit is weggelaten om prestatieproblemen te voorkomen."
+msgstr[1] "%s andere commits zijn weggelaten om prestatieproblemen te voorkomen."
+
+msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr ""
+
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
+msgstr ""
+
+msgid "%{storage_name}: failed storage access attempt on host:"
+msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr "(bekijk de %{link} voor meer info over hoe je het kan installeren)."
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1st contribution!"
+msgstr ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
+msgid "About auto deploy"
+msgstr "Over auto deploy"
+
+msgid "Abuse Reports"
+msgstr "Misbruik rapporten"
+
+msgid "Access Tokens"
+msgstr "Toegangstokens"
+
+msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
+msgstr ""
+
+msgid "Account"
+msgstr "Account"
+
+msgid "Active"
+msgstr "Actief"
+
+msgid "Activity"
+msgstr "Activiteit"
+
+msgid "Add Changelog"
+msgstr "Changelog toevoegen"
+
+msgid "Add Contribution guide"
+msgstr ""
+
+msgid "Add License"
+msgstr "Licentie toevoegen"
+
+msgid "Add an SSH key to your profile to pull or push via SSH."
+msgstr ""
+
+msgid "Add new directory"
+msgstr "Nieuwe map toevoegen"
+
+msgid "All"
+msgstr "Alles"
+
+msgid "Appearance"
+msgstr ""
+
+msgid "Applications"
+msgstr "Applicaties"
+
+msgid "Archived project! Repository is read-only"
+msgstr ""
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
+msgid "Are you sure you want to discard your changes?"
+msgstr ""
+
+msgid "Are you sure you want to reset registration token?"
+msgstr ""
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr ""
+
+msgid "Are you sure?"
+msgstr ""
+
+msgid "Artifacts"
+msgstr ""
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr ""
+
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
+msgstr ""
+
+msgid "Billing"
+msgstr ""
+
+msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgstr ""
+
+msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgstr ""
+
+msgid "BillingPlans|Current plan"
+msgstr ""
+
+msgid "BillingPlans|Customer Support"
+msgstr ""
+
+msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgstr ""
+
+msgid "BillingPlans|Manage plan"
+msgstr ""
+
+msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgstr ""
+
+msgid "BillingPlans|See all %{plan_name} features"
+msgstr ""
+
+msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgstr ""
+
+msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgstr ""
+
+msgid "BillingPlans|Upgrade"
+msgstr ""
+
+msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgstr ""
+
+msgid "BillingPlans|frequently asked questions"
+msgstr ""
+
+msgid "BillingPlans|monthly"
+msgstr ""
+
+msgid "BillingPlans|paid annually at %{price_per_year}"
+msgstr ""
+
+msgid "BillingPlans|per user"
+msgstr ""
+
+msgid "Billinglans|Downgrade"
+msgstr ""
+
+msgid "Board"
+msgstr ""
+
+msgid "Branch"
+msgid_plural "Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
+msgstr ""
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
+msgid "Branches"
+msgstr "Branches"
+
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr "Bladeren in map"
+
+msgid "Browse File"
+msgstr "Bekijk bestand"
+
+msgid "Browse Files"
+msgstr "Door bestanden bladeren"
+
+msgid "Browse files"
+msgstr "Door bestanden bladeren"
+
+msgid "ByAuthor|by"
+msgstr "door"
+
+msgid "CI / CD"
+msgstr "CI / CD"
+
+msgid "CI configuration"
+msgstr "CI Configuratie"
+
+msgid "Cancel"
+msgstr "Annuleren"
+
+msgid "Cancel edit"
+msgstr "Bewerken annuleren"
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr ""
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr ""
+
+msgid "ChangeTypeAction|Revert"
+msgstr ""
+
+msgid "Changelog"
+msgstr ""
+
+msgid "Charts"
+msgstr "Grafieken"
+
+msgid "Chat"
+msgstr "Chat"
+
+msgid "Cherry-pick this commit"
+msgstr "Cherry-pick deze commit"
+
+msgid "Cherry-pick this merge request"
+msgstr ""
+
+msgid "CiStatusLabel|canceled"
+msgstr "geannuleerd"
+
+msgid "CiStatusLabel|created"
+msgstr "gemaakt"
+
+msgid "CiStatusLabel|failed"
+msgstr "mislukt"
+
+msgid "CiStatusLabel|manual action"
+msgstr ""
+
+msgid "CiStatusLabel|passed"
+msgstr ""
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr ""
+
+msgid "CiStatusLabel|pending"
+msgstr ""
+
+msgid "CiStatusLabel|skipped"
+msgstr "overgeslagen"
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr ""
+
+msgid "CiStatusText|blocked"
+msgstr "geblokkeerd"
+
+msgid "CiStatusText|canceled"
+msgstr ""
+
+msgid "CiStatusText|created"
+msgstr "gemaakt"
+
+msgid "CiStatusText|failed"
+msgstr ""
+
+msgid "CiStatusText|manual"
+msgstr "handmatig"
+
+msgid "CiStatusText|passed"
+msgstr "geslaagd"
+
+msgid "CiStatusText|pending"
+msgstr ""
+
+msgid "CiStatusText|skipped"
+msgstr "overgeslagen"
+
+msgid "CiStatus|running"
+msgstr ""
+
+msgid "Comments"
+msgstr "Opmerkingen"
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
+msgid "Commit message"
+msgstr ""
+
+msgid "CommitBoxTitle|Commit"
+msgstr "Commit"
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr "%{file_name} toevoegen"
+
+msgid "Commits"
+msgstr "Commits"
+
+msgid "Commits feed"
+msgstr ""
+
+msgid "Commits|History"
+msgstr "Geschiedenis"
+
+msgid "Committed by"
+msgstr "Gecommit door"
+
+msgid "Compare"
+msgstr "Vergelijk"
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
+msgstr ""
+
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr ""
+
+msgid "Copy commit SHA to clipboard"
+msgstr ""
+
+msgid "Create New Directory"
+msgstr ""
+
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Create directory"
+msgstr "Maak map aan"
+
+msgid "Create empty bare repository"
+msgstr ""
+
+msgid "Create merge request"
+msgstr ""
+
+msgid "Create new..."
+msgstr ""
+
+msgid "CreateNewFork|Fork"
+msgstr ""
+
+msgid "CreateTag|Tag"
+msgstr ""
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cron syntax"
+msgstr ""
+
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
+msgstr ""
+
+msgid "Cycle Analytics"
+msgstr ""
+
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr ""
+
+msgid "CycleAnalyticsStage|Code"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Production"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Review"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Test"
+msgstr ""
+
+msgid "Define a custom pattern with cron syntax"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Deploy Keys"
+msgstr ""
+
+msgid "Description"
+msgstr ""
+
+msgid "Details"
+msgstr ""
+
+msgid "Directory name"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
+msgid "Don't show again"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Download tar"
+msgstr ""
+
+msgid "Download tar.bz2"
+msgstr ""
+
+msgid "Download tar.gz"
+msgstr ""
+
+msgid "Download zip"
+msgstr "Download zip"
+
+msgid "DownloadArtifacts|Download"
+msgstr ""
+
+msgid "DownloadCommit|Email Patches"
+msgstr ""
+
+msgid "DownloadCommit|Plain Diff"
+msgstr ""
+
+msgid "DownloadSource|Download"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Emails"
+msgstr ""
+
+msgid "Enable in settings"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr ""
+
+msgid "EventFilterBy|Filter by comments"
+msgstr ""
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by push events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by team"
+msgstr ""
+
+msgid "Every day (at 4:00am)"
+msgstr ""
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr ""
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr ""
+
+msgid "Explore projects"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Files"
+msgstr ""
+
+msgid "Filter by commit message"
+msgstr ""
+
+msgid "Find by path"
+msgstr ""
+
+msgid "Find file"
+msgstr ""
+
+msgid "FirstPushedBy|First"
+msgstr ""
+
+msgid "FirstPushedBy|pushed by"
+msgstr ""
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr ""
+
+msgid "From issue creation until deploy to production"
+msgstr ""
+
+msgid "From merge request merge until deploy to production"
+msgstr ""
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
+msgstr ""
+
+msgid "Go to your fork"
+msgstr ""
+
+msgid "GoToYourFork|Fork"
+msgstr ""
+
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
+msgstr ""
+
+msgid "Health Check"
+msgstr ""
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr ""
+
+msgid "HealthCheck|Access token is"
+msgstr ""
+
+msgid "HealthCheck|Healthy"
+msgstr ""
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr ""
+
+msgid "HealthCheck|Unhealthy"
+msgstr ""
+
+msgid "Housekeeping successfully started"
+msgstr ""
+
+msgid "Import repository"
+msgstr ""
+
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
+msgid "Interval Pattern"
+msgstr ""
+
+msgid "Introducing Cycle Analytics"
+msgstr ""
+
+msgid "Issue events"
+msgstr ""
+
+msgid "Issues"
+msgstr ""
+
+msgid "Jobs"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr ""
+
+msgid "LFSStatus|Enabled"
+msgstr ""
+
+msgid "Labels"
+msgstr ""
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Last Pipeline"
+msgstr ""
+
+msgid "Last Update"
+msgstr ""
+
+msgid "Last commit"
+msgstr ""
+
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
+msgid "Learn more in the"
+msgstr ""
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr ""
+
+msgid "Leave group"
+msgstr ""
+
+msgid "Leave project"
+msgstr ""
+
+msgid "License"
+msgstr ""
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Locked Files"
+msgstr ""
+
+msgid "Median"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Merge request"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr ""
+
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] "Nieuwe issue"
+msgstr[1] "Nieuwe issues"
+
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "New branch"
+msgstr ""
+
+msgid "New directory"
+msgstr ""
+
+msgid "New file"
+msgstr ""
+
+msgid "New issue"
+msgstr ""
+
+msgid "New merge request"
+msgstr ""
+
+msgid "New schedule"
+msgstr ""
+
+msgid "New snippet"
+msgstr ""
+
+msgid "New tag"
+msgstr ""
+
+msgid "No repository"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
+msgid "Not available"
+msgstr ""
+
+msgid "Not enough data"
+msgstr ""
+
+msgid "Notification events"
+msgstr ""
+
+msgid "NotificationEvent|Close issue"
+msgstr ""
+
+msgid "NotificationEvent|Close merge request"
+msgstr ""
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr ""
+
+msgid "NotificationEvent|Merge merge request"
+msgstr ""
+
+msgid "NotificationEvent|New issue"
+msgstr ""
+
+msgid "NotificationEvent|New merge request"
+msgstr ""
+
+msgid "NotificationEvent|New note"
+msgstr ""
+
+msgid "NotificationEvent|Reassign issue"
+msgstr ""
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr ""
+
+msgid "NotificationEvent|Reopen issue"
+msgstr ""
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr ""
+
+msgid "NotificationLevel|Custom"
+msgstr ""
+
+msgid "NotificationLevel|Disabled"
+msgstr ""
+
+msgid "NotificationLevel|Global"
+msgstr ""
+
+msgid "NotificationLevel|On mention"
+msgstr ""
+
+msgid "NotificationLevel|Participate"
+msgstr ""
+
+msgid "NotificationLevel|Watch"
+msgstr ""
+
+msgid "Notifications"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr ""
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr ""
+
+msgid "Options"
+msgstr ""
+
+msgid "Overview"
+msgstr ""
+
+msgid "Owner"
+msgstr ""
+
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Pipeline"
+msgstr ""
+
+msgid "Pipeline Health"
+msgstr ""
+
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "Pipeline quota"
+msgstr ""
+
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Input variable key"
+msgstr ""
+
+msgid "PipelineSchedules|Input variable value"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Remove variable row"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
+msgid "PipelineSchedules|Variables"
+msgstr ""
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr ""
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
+msgid "Pipeline|with stage"
+msgstr ""
+
+msgid "Pipeline|with stages"
+msgstr ""
+
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile"
+msgstr ""
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr ""
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr ""
+
+msgid "Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Project details"
+msgstr ""
+
+msgid "Project export could not be deleted."
+msgstr ""
+
+msgid "Project export has been deleted."
+msgstr ""
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr ""
+
+msgid "Project export started. A download link will be sent by email."
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
+msgid "ProjectFeature|Disabled"
+msgstr ""
+
+msgid "ProjectFeature|Everyone with access"
+msgstr ""
+
+msgid "ProjectFeature|Only team members"
+msgstr ""
+
+msgid "ProjectFileTree|Name"
+msgstr ""
+
+msgid "ProjectLastActivity|Never"
+msgstr ""
+
+msgid "ProjectLifecycle|Stage"
+msgstr ""
+
+msgid "ProjectNetworkGraph|Graph"
+msgstr ""
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
+msgid "Read more"
+msgstr ""
+
+msgid "Readme"
+msgstr ""
+
+msgid "RefSwitcher|Branches"
+msgstr ""
+
+msgid "RefSwitcher|Tags"
+msgstr ""
+
+msgid "Registry"
+msgstr ""
+
+msgid "Related Commits"
+msgstr ""
+
+msgid "Related Deployed Jobs"
+msgstr ""
+
+msgid "Related Issues"
+msgstr ""
+
+msgid "Related Jobs"
+msgstr ""
+
+msgid "Related Merge Requests"
+msgstr ""
+
+msgid "Related Merged Requests"
+msgstr ""
+
+msgid "Remind later"
+msgstr ""
+
+msgid "Remove project"
+msgstr ""
+
+msgid "Repository"
+msgstr ""
+
+msgid "Request Access"
+msgstr ""
+
+msgid "Reset git storage health information"
+msgstr ""
+
+msgid "Reset health check access token"
+msgstr ""
+
+msgid "Reset runners registration token"
+msgstr ""
+
+msgid "Revert this commit"
+msgstr ""
+
+msgid "Revert this merge request"
+msgstr ""
+
+msgid "SSH Keys"
+msgstr ""
+
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Schedules"
+msgstr ""
+
+msgid "Scheduling Pipelines"
+msgstr ""
+
+msgid "Search branches and tags"
+msgstr ""
+
+msgid "Select Archive Format"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select existing branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
+msgid "Service Templates"
+msgstr ""
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Set up CI"
+msgstr ""
+
+msgid "Set up Koding"
+msgstr ""
+
+msgid "Set up auto deploy"
+msgstr ""
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr ""
+
+msgid "Settings"
+msgstr ""
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Snippets"
+msgstr ""
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
+msgid "Source code"
+msgstr ""
+
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
+msgid "StarProject|Star"
+msgstr ""
+
+msgid "Starred projects"
+msgstr ""
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr ""
+
+msgid "Start the Runner!"
+msgstr ""
+
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
+msgstr ""
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Tags"
+msgstr ""
+
+msgid "Target Branch"
+msgstr ""
+
+msgid "Team"
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
+msgstr ""
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr ""
+
+msgid "The fork relationship has been removed."
+msgstr ""
+
+msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
+msgstr ""
+
+msgid "The phase of the development lifecycle."
+msgstr ""
+
+msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
+msgstr ""
+
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
+msgstr ""
+
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
+msgstr ""
+
+msgid "The project can be accessed by any logged in user."
+msgstr ""
+
+msgid "The project can be accessed without any authentication."
+msgstr ""
+
+msgid "The repository for this project does not exist."
+msgstr ""
+
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
+msgstr ""
+
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
+msgstr ""
+
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
+msgstr ""
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr ""
+
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
+msgstr ""
+
+msgid "There are problems accessing Git storage: "
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
+msgid "This means you can not push code until you create an empty repository or import existing one."
+msgstr ""
+
+msgid "Time before an issue gets scheduled"
+msgstr ""
+
+msgid "Time before an issue starts implementation"
+msgstr ""
+
+msgid "Time between merge request creation and merge/close"
+msgstr ""
+
+msgid "Time until first merge request"
+msgstr ""
+
+msgid "Timeago|%s days ago"
+msgstr ""
+
+msgid "Timeago|%s days remaining"
+msgstr ""
+
+msgid "Timeago|%s hours remaining"
+msgstr ""
+
+msgid "Timeago|%s minutes ago"
+msgstr ""
+
+msgid "Timeago|%s minutes remaining"
+msgstr ""
+
+msgid "Timeago|%s months ago"
+msgstr ""
+
+msgid "Timeago|%s months remaining"
+msgstr ""
+
+msgid "Timeago|%s seconds remaining"
+msgstr ""
+
+msgid "Timeago|%s weeks ago"
+msgstr ""
+
+msgid "Timeago|%s weeks remaining"
+msgstr ""
+
+msgid "Timeago|%s years ago"
+msgstr ""
+
+msgid "Timeago|%s years remaining"
+msgstr ""
+
+msgid "Timeago|1 day remaining"
+msgstr ""
+
+msgid "Timeago|1 hour remaining"
+msgstr ""
+
+msgid "Timeago|1 minute remaining"
+msgstr ""
+
+msgid "Timeago|1 month remaining"
+msgstr ""
+
+msgid "Timeago|1 week remaining"
+msgstr ""
+
+msgid "Timeago|1 year remaining"
+msgstr ""
+
+msgid "Timeago|Past due"
+msgstr ""
+
+msgid "Timeago|a day ago"
+msgstr ""
+
+msgid "Timeago|a month ago"
+msgstr ""
+
+msgid "Timeago|a week ago"
+msgstr ""
+
+msgid "Timeago|a while"
+msgstr ""
+
+msgid "Timeago|a year ago"
+msgstr ""
+
+msgid "Timeago|about %s hours ago"
+msgstr ""
+
+msgid "Timeago|about a minute ago"
+msgstr ""
+
+msgid "Timeago|about an hour ago"
+msgstr ""
+
+msgid "Timeago|in %s days"
+msgstr ""
+
+msgid "Timeago|in %s hours"
+msgstr ""
+
+msgid "Timeago|in %s minutes"
+msgstr ""
+
+msgid "Timeago|in %s months"
+msgstr ""
+
+msgid "Timeago|in %s seconds"
+msgstr ""
+
+msgid "Timeago|in %s weeks"
+msgstr ""
+
+msgid "Timeago|in %s years"
+msgstr ""
+
+msgid "Timeago|in 1 day"
+msgstr ""
+
+msgid "Timeago|in 1 hour"
+msgstr ""
+
+msgid "Timeago|in 1 minute"
+msgstr ""
+
+msgid "Timeago|in 1 month"
+msgstr ""
+
+msgid "Timeago|in 1 week"
+msgstr ""
+
+msgid "Timeago|in 1 year"
+msgstr ""
+
+msgid "Timeago|less than a minute ago"
+msgstr ""
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|s"
+msgstr ""
+
+msgid "Total Time"
+msgstr ""
+
+msgid "Total test time for all commits/merges"
+msgstr ""
+
+msgid "Unstar"
+msgstr ""
+
+msgid "Upload New File"
+msgstr ""
+
+msgid "Upload file"
+msgstr ""
+
+msgid "UploadLink|click to upload"
+msgstr ""
+
+msgid "Use the following registration token during setup:"
+msgstr ""
+
+msgid "Use your global notification setting"
+msgstr ""
+
+msgid "View file @ "
+msgstr ""
+
+msgid "View open merge request"
+msgstr ""
+
+msgid "View replaced file @ "
+msgstr ""
+
+msgid "VisibilityLevel|Internal"
+msgstr ""
+
+msgid "VisibilityLevel|Private"
+msgstr ""
+
+msgid "VisibilityLevel|Public"
+msgstr ""
+
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr ""
+
+msgid "We don't have enough data to show this stage."
+msgstr ""
+
+msgid "Wiki"
+msgstr ""
+
+msgid "Withdraw Access Request"
+msgstr ""
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You can only add files when you are on a branch"
+msgstr ""
+
+msgid "You have reached your project limit"
+msgstr ""
+
+msgid "You must sign in to star a project"
+msgstr ""
+
+msgid "You need permission."
+msgstr ""
+
+msgid "You will not get any notifications via email"
+msgstr ""
+
+msgid "You will only receive notifications for the events you choose"
+msgstr ""
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr ""
+
+msgid "You will receive notifications for any activity"
+msgstr ""
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr ""
+
+msgid "Your name"
+msgstr ""
+
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "new merge request"
+msgstr ""
+
+msgid "notification emails"
+msgstr ""
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 88ca25dbb3b..318c719c2ed 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:42-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -29,6 +29,9 @@ msgstr[1] "%s commits adicionais foram omitidos para prevenir problemas de perfo
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} fez commit %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr ""
@@ -51,6 +54,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Uma coleção de gráficos sobre Integração Contínua"
@@ -93,7 +99,7 @@ msgstr "Adicionar novo diretório"
msgid "All"
msgstr ""
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -117,10 +123,34 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -177,6 +207,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -194,6 +227,90 @@ msgstr "Mudar de branch"
msgid "Branches"
msgstr ""
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Navegar no Diretório"
@@ -337,9 +454,6 @@ msgstr "Commit feito por"
msgid "Compare"
msgstr "Comparar"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Guia de contribuição"
@@ -489,6 +603,9 @@ msgstr "Alterar Agendamento do Pipeline %{id}"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -516,6 +633,9 @@ msgstr "Todos os meses (no dia primeiro às 4:00)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Toda semana (domingos às 4:00)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Erro ao alterar o proprietário"
@@ -572,7 +692,28 @@ msgstr "Ir para seu fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -593,12 +734,6 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
-msgid "Home"
-msgstr "Início"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "Manutenção iniciada com sucesso"
@@ -620,6 +755,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Desabilitado"
@@ -684,6 +822,9 @@ msgstr ""
msgid "Merge events"
msgstr ""
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -812,6 +953,18 @@ msgstr ""
msgid "Owner"
msgstr "Proprietário"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -917,10 +1070,7 @@ msgstr "com etapas"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -953,12 +1103,6 @@ msgstr "O link para a exportação do projeto expirou. Favor gerar uma nova expo
msgid "Project export started. A download link will be sent by email."
msgstr "Exportação do projeto iniciada. Um link para baixá-la será enviado por email."
-msgid "Project home"
-msgstr "Página inicial do projeto"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
@@ -983,6 +1127,27 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -1001,6 +1166,9 @@ msgstr "Branches"
msgid "RefSwitcher|Tags"
msgstr "Tags"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Commits Relacionados"
@@ -1055,6 +1223,9 @@ msgstr "Salvar agendamento da pipeline"
msgid "Schedule a new pipeline"
msgstr "Agendar nova pipeline"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Agendando pipelines"
@@ -1094,6 +1265,12 @@ msgstr "defina uma senha"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
@@ -1102,6 +1279,102 @@ msgstr[1] "Mostrando %d eventos"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "Código-fonte"
@@ -1114,6 +1387,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr "Marcar"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Iniciar um %{new_merge_request} a partir dessas alterações"
@@ -1123,6 +1399,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr "Trocar branch/tag"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] ""
@@ -1188,6 +1467,9 @@ msgstr "O valor situado no ponto médio de uma série de valores observados. Ex.
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Isto significa que você não pode entregar código até que crie um repositório vazio ou importe um existente."
@@ -1363,9 +1645,15 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Utilizar configuração de notificação global"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "Ver merge request aberto"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -1438,6 +1726,12 @@ msgstr "Você não conseguirá fazer pull ou push no projeto via SSH até que a
msgid "Your name"
msgstr "Seu nome"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "dia"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 96e6c8a8d3f..507dc187cdb 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:43-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -31,23 +31,26 @@ msgstr[2] "%s добавленные коммиты были иÑключены
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} коммичено %{commit_timeago}"
-msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr "%{number_of_failures} из %{maximum_failures} возможных попыток. Ð’Ñ‹ можете попытатьÑÑ ÐµÑ‰Ðµ раз."
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} из %{maximum_failures} возможных неудачных попыток. GitLab заблокирует доÑтуп на %{number_of_seconds} Ñекунд."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} из %{maximum_failures} возможных неудачных попыток. GitLab не будет автоматичеÑки повторÑÑ‚ÑŒ попытку. СброÑьте информацию хранилища поÑле уÑÑ‚Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%{storage_name}: Ð½ÐµÑƒÐ´Ð°Ñ‡Ð½Ð°Ñ Ð¿Ð¾Ð¿Ñ‹Ñ‚ÐºÐ° доÑтупа к хранилищу на хоÑте:"
+msgstr[1] "%{storage_name}: %{failed_attempts} - неудачные попытки доÑтупа к хранилищу:"
+msgstr[2] "%{storage_name}: %{failed_attempts} - неудачные попытки доÑтупа к хранилищу:"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(перейдите по ÑÑылке %{link} Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¸ об уÑтановке)."
msgid "1 pipeline"
msgid_plural "%d pipelines"
@@ -55,23 +58,26 @@ msgstr[0] "1 конвейер"
msgstr[1] "%d конвейеры"
msgstr[2] "%d конвейеры"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Графики отноÑительно непрерывной интеграции"
msgid "About auto deploy"
-msgstr "ÐвтоматичеÑкое развертывание"
+msgstr "Об автоматичеÑком развёртывании"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Отчёты о Жалобах"
msgid "Access Tokens"
-msgstr ""
+msgstr "Токены ДоÑтупа"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr ""
+msgstr "ДоÑтуп к вышедшим из ÑÑ‚Ñ€Ð¾Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰Ð°Ð¼ временно отключен Ð´Ð»Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñти Ð¼Ð¾Ð½Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² целÑÑ… воÑÑтановлениÑ. СброÑьте информацию о хранилищах поÑле уÑÑ‚Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹, чтобы разрешить доÑтуп."
msgid "Account"
-msgstr ""
+msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
msgid "Active"
msgstr "Ðктивный"
@@ -95,13 +101,13 @@ msgid "Add new directory"
msgstr "Добавить каталог"
msgid "All"
-msgstr ""
+msgstr "Ð’Ñе"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "ПриложениÑ"
msgid "Archived project! Repository is read-only"
msgstr "Ðрхивный проект! Репозиторий доÑтупен только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
@@ -113,18 +119,42 @@ msgid "Are you sure you want to discard your changes?"
msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите отменить Ваши изменениÑ?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите ÑброÑить Ñтот ключ региÑтрации?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите ÑброÑить Ñтот ключ проверки работоÑпоÑобноÑти?"
msgid "Are you sure?"
msgstr "Вы уверены?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -181,6 +211,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Ветка"
@@ -199,6 +232,90 @@ msgstr "Переключить ветку"
msgid "Branches"
msgstr "Ветки"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "Обзор"
@@ -215,7 +332,7 @@ msgid "ByAuthor|by"
msgstr "по автору"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "ÐаÑтройка CI"
@@ -245,7 +362,7 @@ msgid "Charts"
msgstr "Диаграммы"
msgid "Chat"
-msgstr ""
+msgstr "Чат"
msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
@@ -343,9 +460,6 @@ msgstr "ФикÑировано"
msgid "Compare"
msgstr "Сравнить"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "РуководÑтво учаÑтника"
@@ -443,13 +557,13 @@ msgstr[1] "Размещение"
msgstr[2] "Размещение"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Ключи РазвертываниÑ"
msgid "Description"
msgstr "ОпиÑание"
msgid "Details"
-msgstr ""
+msgstr "ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
msgid "Directory name"
msgstr "Каталог"
@@ -494,25 +608,28 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Изменить раÑпиÑание конвейера %{id}"
msgid "Emails"
+msgstr "Email-адреÑа"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
-msgstr ""
+msgstr "Фильтр по вÑему"
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "Фильтр по комментарию"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "Фильтр по ÑобытиÑм обÑуждений"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "Фильтр по ÑобытиÑм ÑлиÑний"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "Фильтр по ÑобытиÑм отправки"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "Фильтр по команде"
msgid "Every day (at 4:00am)"
msgstr "Ежедневно (в 4:00)"
@@ -523,6 +640,9 @@ msgstr "ЕжемеÑÑчно (каждое 1-е чиÑло в 4:00)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Еженедельно (по воÑкреÑениÑми в 4:00)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Ðе удалоÑÑŒ изменить владельца"
@@ -563,13 +683,13 @@ msgid "From merge request merge until deploy to production"
msgstr "От запроÑа на ÑлиÑние до Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² рабочей Ñреде"
msgid "GPG Keys"
-msgstr ""
+msgstr "GPG Ключи"
msgid "Geo Nodes"
msgstr ""
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑтабильноÑти Git хранилища была Ñброшена"
msgid "GitLab Runner section"
msgstr "Ð¡ÐµÐºÑ†Ð¸Ñ Gitlab Runner"
@@ -580,33 +700,48 @@ msgstr "Перейти к вашему форку"
msgid "GoToYourFork|Fork"
msgstr "Форк"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
-msgid "Health Check"
+msgid "GroupSettings|Share with group lock"
msgstr ""
-msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
-msgid "HealthCheck|Access token is"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|Healthy"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|No Health Problems Detected"
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
msgstr ""
-msgid "HealthCheck|Unhealthy"
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
-msgid "Home"
-msgstr "ГлавнаÑ"
-
-msgid "Hooks"
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Health Check"
+msgstr "Проверка работоÑпоÑобноÑти"
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ работоÑпоÑобноÑти может быть получена из Ñледующих точек подключениÑ. ДоÑтупна более Ð¿Ð¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
+
+msgid "HealthCheck|Access token is"
+msgstr "Ключ доÑтупа - "
+
+msgid "HealthCheck|Healthy"
+msgstr "Стабильно"
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr "Проблем работоÑпоÑобноÑти не обнаружено"
+
+msgid "HealthCheck|Unhealthy"
+msgstr "ÐеÑтабильный"
+
msgid "Housekeeping successfully started"
msgstr "ОчиÑтка уÑпешно запущена"
@@ -623,9 +758,12 @@ msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
msgid "Issue events"
-msgstr ""
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸"
msgid "Issues"
+msgstr "Задачи"
+
+msgid "Jobs"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -635,7 +773,7 @@ msgid "LFSStatus|Enabled"
msgstr "Включено"
msgid "Labels"
-msgstr ""
+msgstr "Метки"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -653,10 +791,10 @@ msgid "Last commit"
msgstr "ПоÑледний коммит"
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "Вы отправили в"
msgid "LastPushEvent|at"
-msgstr ""
+msgstr "в"
msgid "Learn more in the"
msgstr "Узнайте больше в"
@@ -686,25 +824,28 @@ msgid "Median"
msgstr "Среднее"
msgid "Members"
-msgstr ""
+msgstr "УчаÑтники"
msgid "Merge Requests"
-msgstr ""
+msgstr "ЗапроÑÑ‹ на СлиÑние"
msgid "Merge events"
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ ÑлиÑний"
+
+msgid "Merge request"
msgstr ""
msgid "Messages"
-msgstr ""
+msgstr "СообщениÑ"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавить ключ SSH"
msgid "Monitoring"
-msgstr ""
+msgstr "Мониторинг"
msgid "More information is available|here"
-msgstr ""
+msgstr "Больше информации доÑтупно|тут"
msgid "New Issue"
msgid_plural "New Issues"
@@ -806,7 +947,7 @@ msgid "NotificationLevel|Watch"
msgstr "ОтÑлеживать"
msgid "Notifications"
-msgstr ""
+msgstr "УведомлениÑ"
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
@@ -818,14 +959,26 @@ msgid "Options"
msgstr "ÐаÑтройки"
msgid "Overview"
-msgstr ""
+msgstr "Обзор"
msgid "Owner"
msgstr "Владелец"
-msgid "Password"
+msgid "Pagination|Last »"
msgstr ""
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr "Пароль"
+
msgid "Pipeline"
msgstr "Конвейер"
@@ -905,13 +1058,13 @@ msgid "Pipelines charts"
msgstr "Диаграмма конвейера"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Конвеер за поÑледний меÑÑц"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Конвеер за поÑледнюю неделю"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Конвееры за поÑледний год"
msgid "Pipeline|all"
msgstr "вÑе"
@@ -926,12 +1079,9 @@ msgid "Pipeline|with stages"
msgstr "Ñо ÑтадиÑми"
msgid "Preferences"
-msgstr ""
-
-msgid "Profile Settings"
-msgstr ""
+msgstr "ПредпочтениÑ"
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -950,7 +1100,7 @@ msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп к проекту должен предоÑтавлÑÑ‚ÑŒÑÑ Ñвно каждому пользователю."
msgid "Project details"
-msgstr ""
+msgstr "Детали проекта"
msgid "Project export could not be deleted."
msgstr "Ðевозможно удалить ÑкÑпорт проекта."
@@ -964,14 +1114,8 @@ msgstr "ИÑтек Ñрок дейÑÑ‚Ð²Ð¸Ñ ÑÑылки на проект. СÐ
msgid "Project export started. A download link will be sent by email."
msgstr "Ðачат ÑкÑпорт проекта. СÑылка Ð´Ð»Ñ ÑÐºÐ°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ отправлена по Ñлектронной почте."
-msgid "Project home"
-msgstr "ДомашнÑÑ Ñтраница"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "ПодпиÑатьÑÑ"
msgid "ProjectFeature|Disabled"
msgstr "Отключено"
@@ -994,17 +1138,38 @@ msgstr "Этап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr "Загрузка проектов"
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr "Проекты, которые вы чаÑто поÑещаете, будут отображатьÑÑ Ð·Ð´ÐµÑÑŒ"
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr "ПоиÑк по вашим проектам"
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr "К Ñожалению, по вашему запроÑу проекты не найдены"
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr "Эта функциональноÑÑ‚ÑŒ требует поддержки localStorage в вашем браузере"
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
-msgstr ""
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
msgid "Read more"
msgstr "Подробнее"
msgid "Readme"
-msgstr ""
+msgstr "ИнÑтрукциÑ"
msgid "RefSwitcher|Branches"
msgstr "Ветки"
@@ -1012,6 +1177,9 @@ msgstr "Ветки"
msgid "RefSwitcher|Tags"
msgstr "Теги"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "СвÑзанные коммиты"
@@ -1037,19 +1205,19 @@ msgid "Remove project"
msgstr "Удалить проект"
msgid "Repository"
-msgstr ""
+msgstr "Репозиторий"
msgid "Request Access"
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
msgid "Reset git storage health information"
-msgstr ""
+msgstr "СброÑить информацию о работоÑпоÑобноÑти Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ git"
msgid "Reset health check access token"
-msgstr ""
+msgstr "СброÑить ключ доÑтупа проверки работоÑпоÑобноÑти"
msgid "Reset runners registration token"
-msgstr ""
+msgstr "СброÑить ключ региÑтрации Gitlab Runners"
msgid "Revert this commit"
msgstr "Отменить Ñто изменение"
@@ -1058,7 +1226,7 @@ msgid "Revert this merge request"
msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "SSH Keys"
-msgstr ""
+msgstr "SSH Ключи"
msgid "Save pipeline schedule"
msgstr "Сохранить раÑпиÑание конвейра"
@@ -1066,6 +1234,9 @@ msgstr "Сохранить раÑпиÑание конвейра"
msgid "Schedule a new pipeline"
msgstr "РаÑпиÑание нового конвейера"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Планирование конвейеров"
@@ -1079,13 +1250,13 @@ msgid "Select a timezone"
msgstr "Выбор временной зоны"
msgid "Select existing branch"
-msgstr ""
+msgstr "Выбрать ÑущеÑтвующую ветвь"
msgid "Select target branch"
msgstr "Выбор целевой ветки"
msgid "Service Templates"
-msgstr ""
+msgstr "Шаблоны Служб"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через %{protocol}."
@@ -1103,6 +1274,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "уÑтановить пароль"
msgid "Settings"
+msgstr "ÐаÑтройки"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1112,29 +1289,131 @@ msgstr[1] "Показано %d Ñобытий"
msgstr[2] "Показано %d Ñобытий"
msgid "Snippets"
+msgstr "Сниппеты"
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
msgstr ""
msgid "Source code"
msgstr "ИÑходный код"
msgid "Spam Logs"
-msgstr ""
+msgstr "Спам Логи"
msgid "Specify the following URL during the Runner setup:"
-msgstr ""
+msgstr "Укажите Ñледующий URL во Ð²Ñ€ÐµÐ¼Ñ Ð½Ð°Ñтройки Gitlab Runner:"
msgid "StarProject|Star"
msgstr "Отметить"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
msgid "Start the Runner!"
-msgstr ""
+msgstr "ЗапуÑтить GitLab Runner!"
msgid "Switch branch/tag"
msgstr "Переключить ветка/тег"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Тег"
@@ -1148,7 +1427,7 @@ msgid "Target Branch"
msgstr "Ветка"
msgid "Team"
-msgstr ""
+msgstr "Команда"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Ðа Ñтапе напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита до ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние. Данные автоматичеÑки добавÑÑ‚ÑÑ Ð¿Ð¾Ñле того, как вы Ñоздать Ñвой первый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
@@ -1199,6 +1478,9 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "Среднее значение в Ñ€Ñду. Пример: между 3, 5, 9, Ñреднее 5, между 3, 5, 7, 8, Ñреднее (5+7)/2 = 6."
msgid "There are problems accessing Git storage: "
+msgstr "Проблемы Ñ Ð´Ð¾Ñтупом к Git хранилищу: "
+
+msgid "This is the author's first Merge Request to this project. Handle with care."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
@@ -1373,14 +1655,20 @@ msgid "UploadLink|click to upload"
msgstr "кликните Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸"
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "ИÑпользуйте Ñледующий токен региÑтрации в процеÑÑе уÑтановки:"
msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "ПроÑмотреть открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Ограниченный"
@@ -1400,7 +1688,7 @@ msgid "We don't have enough data to show this stage."
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Ñтапу отÑутÑтвует."
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
@@ -1453,6 +1741,12 @@ msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код
msgid "Your name"
msgstr "Ваше имÑ"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "день"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 4d24140f3dc..ffbbe88cc51 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:43-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -31,23 +31,26 @@ msgstr[2] "%s доданих коммітів були виключені длÑ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} комміт %{commit_timeago}"
-msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr "%{number_of_failures} від %{maximum_failures} невдач. GitLab надаÑÑ‚ÑŒ доÑтуп на наÑтупну Ñпробу."
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} із %{maximum_failures} невдач. GitLab заблокує доÑтуп на %{number_of_seconds} Ñекунд."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} від %{maximum_failures} невдач. GitLab автоматично не повторюватиме Ñпробу. Скиньте інформацію Ñховища при уÑуненні проблеми."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%{storage_name}: Ñпроба невдалого доÑтупу до Ñховища на хоÑÑ‚Ñ–:"
+msgstr[1] "%{storage_name}: %{failed_attempts} невдалі Ñпроби доÑтупу до Ñховища:"
+msgstr[2] "%{storage_name}: %{failed_attempts} невдалих Ñпроб доÑтупу до Ñховища:"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(перейдіть за поÑиланнÑм %{link} Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— ÑтоÑовно вÑтановленнÑ)."
msgid "1 pipeline"
msgid_plural "%d pipelines"
@@ -55,6 +58,9 @@ msgstr[0] "1 конвеєр"
msgstr[1] "%d конвеєра"
msgstr[2] "%d конвеєрів"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ€ÐµÑ€Ð²Ð½Ð¾Ñ— інтеграції"
@@ -62,16 +68,16 @@ msgid "About auto deploy"
msgstr "Про авто розгортаннÑ"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Звіти про зловживаннÑ"
msgid "Access Tokens"
-msgstr ""
+msgstr "Токени доÑтупу"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr ""
+msgstr "ДоÑтуп до помилкових Ñховищ тимчаÑово відключений Ð´Ð»Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ÑÑ‚Ñ– Ð¼Ð¾Ð½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° відновленнÑ. Скиньте інформацію про Ñховища піÑÐ»Ñ ÑƒÑÑƒÐ½ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸, щоб дозволити доÑтуп."
msgid "Account"
-msgstr ""
+msgstr "Обліковий запиÑ"
msgid "Active"
msgstr "Ðктивний"
@@ -97,11 +103,11 @@ msgstr "Додати новий каталог"
msgid "All"
msgstr "Ð’ÑÑ–"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "Додатки"
msgid "Archived project! Repository is read-only"
msgstr "Заархівований проект! Репозиторій доÑтупний лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
@@ -116,15 +122,39 @@ msgid "Are you sure you want to reset registration token?"
msgstr "Ви впевнені, що бажаєте Ñкинути реєÑтраційний токен?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Ви впевнені, що Ви хочете Ñкинути цей ключ перевірки працездатноÑÑ‚Ñ–?"
msgid "Are you sure?"
+msgstr "Ви впевнені?"
+
+msgid "Artifacts"
msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -181,6 +211,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Гілка"
@@ -199,6 +232,90 @@ msgstr "Переключити гілку"
msgid "Branches"
msgstr "Гілки"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "ПереглÑнути каталог"
@@ -215,7 +332,7 @@ msgid "ByAuthor|by"
msgstr "від"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
@@ -224,7 +341,7 @@ msgid "Cancel"
msgstr "СкаÑувати"
msgid "Cancel edit"
-msgstr ""
+msgstr "Відмінити правку"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -245,7 +362,7 @@ msgid "Charts"
msgstr "Графіки"
msgid "Chat"
-msgstr ""
+msgstr "Чат"
msgid "Cherry-pick this commit"
msgstr "Cherry-pick в цьому комміті"
@@ -308,7 +425,7 @@ msgid "CiStatus|running"
msgstr "виконуєтьÑÑ"
msgid "Comments"
-msgstr ""
+msgstr "Коментарі"
msgid "Commit"
msgid_plural "Commits"
@@ -343,9 +460,6 @@ msgstr "Комміт від"
msgid "Compare"
msgstr "ПорівнÑти"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "Керівництво контриб’юторів"
@@ -365,7 +479,7 @@ msgid "Create New Directory"
msgstr "Створити новий каталог"
msgid "Create a new branch"
-msgstr ""
+msgstr "Створити нову гілку"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Створити токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккауета, щоб відправлÑти або отримувати через %{protocol}."
@@ -443,19 +557,19 @@ msgstr[1] "РозгортаннÑ"
msgstr[2] "Розгортань"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Ключи Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
msgid "Description"
msgstr "ОпиÑ"
msgid "Details"
-msgstr ""
+msgstr "Деталі"
msgid "Directory name"
msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
msgid "Discard changes"
-msgstr ""
+msgstr "СкаÑувати зміни"
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -494,25 +608,28 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Редагувати Розклад Конвеєра %{id}"
msgid "Emails"
+msgstr "ÐдреÑи електронної пошти"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
-msgstr ""
+msgstr "Ð’ÑÑ–"
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "Коментарю"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "Проблеми"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "Запити на злиттÑ"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "По відправленні комміту"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "За командою"
msgid "Every day (at 4:00am)"
msgstr "Кожен день (в 4:00 ранку)"
@@ -523,6 +640,9 @@ msgstr "Кожен міÑÑць (1-го чиÑла о 4:00 ранку)"
msgid "Every week (Sundays at 4:00am)"
msgstr "Ð©Ð¾Ñ‚Ð¸Ð¶Ð½Ñ (в неділю о 4:00 ранку)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
@@ -563,16 +683,16 @@ msgid "From merge request merge until deploy to production"
msgstr "З об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° ПРОД"
msgid "GPG Keys"
-msgstr ""
+msgstr "GPG ключі"
msgid "Geo Nodes"
msgstr ""
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Git була Ñкинута"
msgid "GitLab Runner section"
-msgstr ""
+msgstr "Розділ GitLab Runner"
msgid "Go to your fork"
msgstr "Перейти до вашого форку"
@@ -580,33 +700,48 @@ msgstr "Перейти до вашого форку"
msgid "GoToYourFork|Fork"
msgstr "Форк"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
-msgid "Health Check"
+msgid "GroupSettings|Share with group lock"
msgstr ""
-msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
-msgid "HealthCheck|Access token is"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|Healthy"
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
msgstr ""
-msgid "HealthCheck|No Health Problems Detected"
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
msgstr ""
-msgid "HealthCheck|Unhealthy"
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
-msgid "Home"
-msgstr "Головна"
-
-msgid "Hooks"
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Health Check"
+msgstr "Перевірки працездатноÑÑ‚Ñ–"
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr "Інформацію про працездатніÑÑ‚ÑŒ можна отримати з наÑтупних ендпойнтів. Більше інформації доÑтупно"
+
+msgid "HealthCheck|Access token is"
+msgstr "Токен доÑтупу Ñ”"
+
+msgid "HealthCheck|Healthy"
+msgstr "Здоровий"
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr "Жодних проблем із здоров'Ñм не виÑвлено"
+
+msgid "HealthCheck|Unhealthy"
+msgstr "Ðездорові"
+
msgid "Housekeeping successfully started"
msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
@@ -614,7 +749,7 @@ msgid "Import repository"
msgstr "Імпорт репозеторіÑ"
msgid "Install a Runner compatible with GitLab CI"
-msgstr ""
+msgstr "Ð’Ñтановіть Runner, ÑуміÑний з GitLab CI"
msgid "Interval Pattern"
msgstr "Шаблон інтервалу"
@@ -623,9 +758,12 @@ msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
msgid "Issue events"
-msgstr ""
+msgstr "Події проблем"
msgid "Issues"
+msgstr "Проблеми"
+
+msgid "Jobs"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -635,7 +773,7 @@ msgid "LFSStatus|Enabled"
msgstr "Увімкнено"
msgid "Labels"
-msgstr ""
+msgstr "Мітки"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -653,10 +791,10 @@ msgid "Last commit"
msgstr "ОÑтанній комміт"
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "Ви надіÑлали зміни до"
msgid "LastPushEvent|at"
-msgstr ""
+msgstr "в"
msgid "Learn more in the"
msgstr "ДізнайтеÑÑŒ більше"
@@ -686,25 +824,28 @@ msgid "Median"
msgstr "Медіана"
msgid "Members"
-msgstr ""
+msgstr "КориÑтувачі"
msgid "Merge Requests"
-msgstr ""
+msgstr "Запит на злиттÑ"
msgid "Merge events"
+msgstr "Події запит на злиттÑ"
+
+msgid "Merge request"
msgstr ""
msgid "Messages"
-msgstr ""
+msgstr "ПовідомленнÑ"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "не додаÑте SSH ключ"
msgid "Monitoring"
-msgstr ""
+msgstr "Моніторинг"
msgid "More information is available|here"
-msgstr ""
+msgstr "тут"
msgid "New Issue"
msgid_plural "New Issues"
@@ -806,7 +947,7 @@ msgid "NotificationLevel|Watch"
msgstr "ВідÑтежувати"
msgid "Notifications"
-msgstr ""
+msgstr "СповіщеннÑ"
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
@@ -818,14 +959,26 @@ msgid "Options"
msgstr "Параметри"
msgid "Overview"
-msgstr ""
+msgstr "ОглÑд"
msgid "Owner"
msgstr "ВлаÑник"
-msgid "Password"
+msgid "Pagination|Last »"
msgstr ""
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr "Пароль"
+
msgid "Pipeline"
msgstr "Конвеєр"
@@ -905,13 +1058,13 @@ msgid "Pipelines charts"
msgstr "Чарти Конвеєрів"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Конвеєри за оÑтанній міÑÑць"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Конвеєри за оÑтанній тиждень"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Конвеєри за оÑтанній рік"
msgid "Pipeline|all"
msgstr "вÑÑ–"
@@ -926,12 +1079,9 @@ msgid "Pipeline|with stages"
msgstr "зі ÑтадіÑми"
msgid "Preferences"
-msgstr ""
+msgstr "ÐалаштуваннÑ"
-msgid "Profile Settings"
-msgstr ""
-
-msgid "Project"
+msgid "Profile"
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -950,7 +1100,7 @@ msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
msgid "Project details"
-msgstr ""
+msgstr "Деталі проекту"
msgid "Project export could not be deleted."
msgstr "Ðеможливо видалити екÑпорт проекту."
@@ -964,14 +1114,8 @@ msgstr "ЗакінчивÑÑ Ñ‚ÐµÑ€Ð¼Ñ–Ð½ дії поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° проÐ
msgid "Project export started. A download link will be sent by email."
msgstr "Розпочато екÑпорт проекту. ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ ÑÐºÐ°Ñ‡ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ надіÑлана електронною поштою."
-msgid "Project home"
-msgstr "Ð”Ð¾Ð¼Ð°ÑˆÐ½Ñ Ñторінка проекту"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "ПідпиÑатиÑÑ"
msgid "ProjectFeature|Disabled"
msgstr "Вимкнено"
@@ -994,11 +1138,32 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr "Проекти, Ñкі ви чаÑто відвідуєте, будуть відображені тут"
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr "Пошук по ваших проектах"
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr "Ðа жаль, по вашоу запиту проектів не знайдено"
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±ÑƒÑ” підтримки localStorage вашим браузером"
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
-msgstr ""
+msgstr "Push події"
msgid "Read more"
msgstr "Докладніше"
@@ -1012,6 +1177,9 @@ msgstr "Гілки"
msgid "RefSwitcher|Tags"
msgstr "Теги"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "Пов'Ñзані Комміти"
@@ -1037,19 +1205,19 @@ msgid "Remove project"
msgstr "Видалити проект"
msgid "Repository"
-msgstr ""
+msgstr "Репозиторій"
msgid "Request Access"
msgstr "Запит доÑтупу"
msgid "Reset git storage health information"
-msgstr ""
+msgstr "Скиньте інформацію про працездатніÑÑ‚ÑŒ Ñховища git"
msgid "Reset health check access token"
-msgstr ""
+msgstr "Скиньте токен доÑтупу Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ перевірки працездатноÑÑ‚Ñ–"
msgid "Reset runners registration token"
-msgstr ""
+msgstr "Скинути реєÑтраційний токен runner-ів"
msgid "Revert this commit"
msgstr "СкаÑувати цей комміт"
@@ -1058,7 +1226,7 @@ msgid "Revert this merge request"
msgstr "СкаÑувати цей запит на злиттÑ"
msgid "SSH Keys"
-msgstr ""
+msgstr "Ключі SSH"
msgid "Save pipeline schedule"
msgstr "Зберегти Розклад Конвеєра"
@@ -1066,6 +1234,9 @@ msgstr "Зберегти Розклад Конвеєра"
msgid "Schedule a new pipeline"
msgstr "Розклад нового конвеєра"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
@@ -1079,13 +1250,13 @@ msgid "Select a timezone"
msgstr "Вибрати чаÑовий поÑÑ"
msgid "Select existing branch"
-msgstr ""
+msgstr "Виберіть гілку"
msgid "Select target branch"
msgstr "Вибір цільової гілки"
msgid "Service Templates"
-msgstr ""
+msgstr "Ð¡ÐµÑ€Ð²Ñ–Ñ ÑˆÐ°Ð±Ð»Ð¾Ð½Ñ–Ð²"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати код через %{protocol}."
@@ -1103,6 +1274,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "вÑтановити пароль"
msgid "Settings"
+msgstr "ÐалаштуваннÑ"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1112,29 +1289,131 @@ msgstr[1] "Показано %d події"
msgstr[2] "Показано %d подій"
msgid "Snippets"
+msgstr "Фрагменти"
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
msgstr ""
msgid "Source code"
msgstr "Код"
msgid "Spam Logs"
-msgstr ""
+msgstr "Спам-журнал"
msgid "Specify the following URL during the Runner setup:"
-msgstr ""
+msgstr "Зазначте наÑтупний URL під Ñ‡Ð°Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Runner-а:"
msgid "StarProject|Star"
msgstr "ПідпиÑатиÑÑ"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Почати %{new_merge_request} з цих змін"
msgid "Start the Runner!"
-msgstr ""
+msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Runner!"
msgid "Switch branch/tag"
msgstr "тег"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Тег"
@@ -1148,7 +1427,7 @@ msgid "Target Branch"
msgstr "Цільова гілка"
msgid "Team"
-msgstr ""
+msgstr "Команда"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Ðа Ñтадії напиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ, показує Ñ‡Ð°Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ комміту до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на об'єднаннÑ. Дані будуть автоматично додані піÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ першого запиту на об'єднаннÑ."
@@ -1199,6 +1478,9 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3, 5, 9, Ñередніми 5, між 3, 5, 7, 8, Ñередніми (5 + 7) / 2 = 6."
msgid "There are problems accessing Git storage: "
+msgstr "Є проблеми з доÑтупом до Ñховища: "
+
+msgid "This is the author's first Merge Request to this project. Handle with care."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
@@ -1373,14 +1655,20 @@ msgid "UploadLink|click to upload"
msgstr "ÐатиÑніть, щоб завантажити"
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "ВикориÑтовувати токен під Ñ‡Ð°Ñ ÑƒÑтановки:"
msgid "Use your global notification setting"
msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "ПереглÑд відкритих запитів на злиттÑ"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "Внутрішній"
@@ -1400,7 +1688,7 @@ msgid "We don't have enough data to show this stage."
msgstr "Ми не маємо доÑтатньо даних Ð´Ð»Ñ Ð¿Ð¾ÐºÐ°Ð·Ñƒ цього етапу."
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "СкаÑувати запит доÑтупу"
@@ -1453,6 +1741,12 @@ msgstr "Ви не зможете отримувати Ñ– відправлÑти
msgid "Your name"
msgstr "Ваше ім'Ñ"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "день"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 47de28209df..4a05b159008 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:44-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -27,6 +27,9 @@ msgstr[0] "为æ高页é¢åŠ è½½é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交于 %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "已失败 %{number_of_failures} 次/最多å…许失败失败 %{maximum_failures} 次,GitLab 将继续é‡è¯•ã€‚"
@@ -47,6 +50,9 @@ msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¡æµæ°´çº¿"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -54,16 +60,16 @@ msgid "About auto deploy"
msgstr "关于自动部署"
msgid "Abuse Reports"
-msgstr ""
+msgstr "滥用报告"
msgid "Access Tokens"
-msgstr ""
+msgstr "访问令牌"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "为方便修å¤æŒ‚载问题,访问故障存储已被暂时ç¦ç”¨ã€‚在问题解决åŽè¯·é‡ç½®å­˜å‚¨å¥åº·ä¿¡æ¯ï¼Œä»¥å…许å†æ¬¡è®¿é—®ã€‚"
msgid "Account"
-msgstr ""
+msgstr "å¸å·"
msgid "Active"
msgstr "å¯ç”¨"
@@ -89,11 +95,11 @@ msgstr "添加目录"
msgid "All"
msgstr "全部"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "应用程åº"
msgid "Archived project! Repository is read-only"
msgstr "项目已归档ï¼å­˜å‚¨åº“为åªè¯»çŠ¶æ€"
@@ -113,10 +119,34 @@ msgstr "确定è¦é‡ç½®å¥åº·æ£€æŸ¥ä»¤ç‰Œå—?"
msgid "Are you sure?"
msgstr "确定å—?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -173,6 +203,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "分支"
@@ -189,6 +222,90 @@ msgstr "切æ¢åˆ†æ”¯"
msgid "Branches"
msgstr "分支"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "æµè§ˆç›®å½•"
@@ -205,7 +322,7 @@ msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI é…ç½®"
@@ -235,7 +352,7 @@ msgid "Charts"
msgstr "统计图"
msgid "Chat"
-msgstr ""
+msgstr "交æµ"
msgid "Cherry-pick this commit"
msgstr "优选此æ交"
@@ -331,9 +448,6 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比较"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "贡献指å—"
@@ -429,7 +543,7 @@ msgid_plural "Deploys"
msgstr[0] "部署"
msgid "Deploy Keys"
-msgstr ""
+msgstr "部署密钥"
msgid "Description"
msgstr "æè¿°"
@@ -480,6 +594,9 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
msgid "Emails"
+msgstr "电å­é‚®ä»¶"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
@@ -509,6 +626,9 @@ msgstr "æ¯æœˆæ‰§è¡Œï¼ˆæ¯æœˆ 1 日凌晨 4 点)"
msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯å‘¨æ‰§è¡Œï¼ˆå‘¨æ—¥å‡Œæ™¨ 4 点)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "无法å˜æ›´æ‰€æœ‰è€…"
@@ -547,7 +667,7 @@ msgid "From merge request merge until deploy to production"
msgstr "从åˆå¹¶è¯·æ±‚被åˆå¹¶åŽåˆ°éƒ¨ç½²è‡³ç”Ÿäº§çŽ¯å¢ƒ"
msgid "GPG Keys"
-msgstr ""
+msgstr "GPG 密钥"
msgid "Geo Nodes"
msgstr ""
@@ -564,7 +684,28 @@ msgstr "跳转到派生项目"
msgid "GoToYourFork|Fork"
msgstr "跳转到派生项目"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -585,12 +726,6 @@ msgstr "没有检测到å¥åº·é—®é¢˜"
msgid "HealthCheck|Unhealthy"
msgstr "éžå¥åº·"
-msgid "Home"
-msgstr "首页"
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "已开始维护"
@@ -610,6 +745,9 @@ msgid "Issue events"
msgstr "议题事件"
msgid "Issues"
+msgstr "议题"
+
+msgid "Jobs"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -619,7 +757,7 @@ msgid "LFSStatus|Enabled"
msgstr "å¯ç”¨"
msgid "Labels"
-msgstr ""
+msgstr "标签"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -666,22 +804,25 @@ msgid "Median"
msgstr "中ä½æ•°"
msgid "Members"
-msgstr ""
+msgstr "æˆå‘˜"
msgid "Merge Requests"
-msgstr ""
+msgstr "åˆå¹¶è¯·æ±‚"
msgid "Merge events"
msgstr "åˆå¹¶äº‹ä»¶"
-msgid "Messages"
+msgid "Merge request"
msgstr ""
+msgid "Messages"
+msgstr "消æ¯"
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新建 SSH 公钥"
msgid "Monitoring"
-msgstr ""
+msgstr "监控"
msgid "More information is available|here"
msgstr "帮助文档"
@@ -784,7 +925,7 @@ msgid "NotificationLevel|Watch"
msgstr "关注"
msgid "Notifications"
-msgstr ""
+msgstr "通知"
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
@@ -796,14 +937,26 @@ msgid "Options"
msgstr "æ“作"
msgid "Overview"
-msgstr ""
+msgstr "概览"
msgid "Owner"
msgstr "所有者"
-msgid "Password"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
msgstr ""
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Password"
+msgstr "密ç "
+
msgid "Pipeline"
msgstr "æµæ°´çº¿"
@@ -883,13 +1036,13 @@ msgid "Pipelines charts"
msgstr "æµæ°´çº¿ç»Ÿè®¡å›¾"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "上个月的æµæ°´çº¿"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "上周的æµæ°´çº¿"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "去年的æµæ°´çº¿"
msgid "Pipeline|all"
msgstr "所有"
@@ -904,14 +1057,11 @@ msgid "Pipeline|with stages"
msgstr "于阶段"
msgid "Preferences"
-msgstr ""
+msgstr "å好设置"
-msgid "Profile Settings"
+msgid "Profile"
msgstr ""
-msgid "Project"
-msgstr "项目"
-
msgid "Project '%{project_name}' queued for deletion."
msgstr "项目 '%{project_name}' 已进入删除队列。"
@@ -942,12 +1092,6 @@ msgstr "项目导出链接已过期。请从项目设置中é‡æ–°ç”Ÿæˆé¡¹ç›®å¯¼
msgid "Project export started. A download link will be sent by email."
msgstr "项目导出已开始。下载链接将通过电å­é‚®ä»¶å‘é€ã€‚"
-msgid "Project home"
-msgstr "项目首页"
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr "订阅"
@@ -972,6 +1116,27 @@ msgstr "阶段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支图"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr "加载项目中"
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr "您ç»å¸¸è®¿é—®çš„项目将出现在这里"
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr "æœç´¢æ‚¨çš„项目"
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr "此功能需è¦æµè§ˆå™¨æ”¯æŒ localStorage"
+
msgid "Push Rules"
msgstr ""
@@ -990,6 +1155,9 @@ msgstr "分支"
msgid "RefSwitcher|Tags"
msgstr "标签"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "相关的æ交"
@@ -1036,7 +1204,7 @@ msgid "Revert this merge request"
msgstr "还原此åˆå¹¶è¯·æ±‚"
msgid "SSH Keys"
-msgstr ""
+msgstr "SSH 密钥"
msgid "Save pipeline schedule"
msgstr "ä¿å­˜æµæ°´çº¿è®¡åˆ’"
@@ -1044,6 +1212,9 @@ msgstr "ä¿å­˜æµæ°´çº¿è®¡åˆ’"
msgid "Schedule a new pipeline"
msgstr "新建æµæ°´çº¿è®¡åˆ’"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "æµæ°´çº¿è®¡åˆ’"
@@ -1063,7 +1234,7 @@ msgid "Select target branch"
msgstr "选择目标分支"
msgid "Service Templates"
-msgstr ""
+msgstr "æœåŠ¡æ¨¡æ¿"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "为账å·åˆ›å»ºä¸€ä¸ªç”¨äºŽæŽ¨é€æˆ–拉å–çš„ %{protocol} 密ç ã€‚"
@@ -1081,6 +1252,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "设置密ç "
msgid "Settings"
+msgstr "设置"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1088,13 +1265,109 @@ msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
msgid "Snippets"
+msgstr "代ç ç‰‡æ®µ"
+
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
msgstr ""
msgid "Source code"
msgstr "æºä»£ç "
msgid "Spam Logs"
-msgstr ""
+msgstr "垃圾信æ¯æ—¥å¿—"
msgid "Specify the following URL during the Runner setup:"
msgstr "在 Runner 设置时指定以下 URL:"
@@ -1102,6 +1375,9 @@ msgstr "在 Runner 设置时指定以下 URL:"
msgid "StarProject|Star"
msgstr "星标"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "由此更改 %{new_merge_request}"
@@ -1111,6 +1387,9 @@ msgstr "å¯åŠ¨ Runner!"
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "标签"
@@ -1175,6 +1454,9 @@ msgstr "中ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间ï
msgid "There are problems accessing Git storage: "
msgstr "访问 Git 存储时出现问题:"
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "在创建一个空的存储库或导入现有存储库之å‰ï¼Œå°†æ— æ³•æŽ¨é€ä»£ç ã€‚"
@@ -1348,9 +1630,15 @@ msgstr "在安装过程中使用以下注册令牌:"
msgid "Use your global notification setting"
msgstr "使用全局通知设置"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "查看待处ç†çš„åˆå¹¶è¯·æ±‚"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "内部"
@@ -1370,7 +1658,7 @@ msgid "We don't have enough data to show this stage."
msgstr "该阶段的数æ®ä¸è¶³ï¼Œæ— æ³•æ˜¾ç¤ºã€‚"
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "å–消æƒé™ç”³è¯·"
@@ -1423,6 +1711,12 @@ msgstr "在账å·ä¸­ %{add_ssh_key_link} 之å‰å°†æ— æ³•é€šè¿‡ SSH 拉å–或推é
msgid "Your name"
msgstr "您的åå­—"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "天"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index fee0d661c7a..c3b6cc72aed 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:44-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -27,6 +27,9 @@ msgstr[0] "為æ高é é¢åŠ è¼‰é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交於 %{commit_timeago}"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "已失敗 %{number_of_failures} 次,最大失敗 %{maximum_failures} 次,GitLab å°‡é‡è©¦ã€‚"
@@ -47,6 +50,9 @@ msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -89,7 +95,7 @@ msgstr "添加新目錄"
msgid "All"
msgstr "全部"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
@@ -113,10 +119,34 @@ msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥ä»¤ç‰Œå—Žï¼Ÿ"
msgid "Are you sure?"
msgstr "確定嗎?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此處或者 %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -173,6 +203,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "分支"
@@ -189,6 +222,90 @@ msgstr "切æ›åˆ†æ”¯"
msgid "Branches"
msgstr "分支"
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "ç€è¦½ç›®éŒ„"
@@ -331,9 +448,6 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比較"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "è²¢ç»æŒ‡å—"
@@ -482,6 +596,9 @@ msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
msgid "Emails"
msgstr ""
+msgid "Enable in settings"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -509,6 +626,9 @@ msgstr "æ¯æœˆåŸ·è¡Œï¼ˆæ¯æœˆ 1 日淩晨 4 點)"
msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯é€±åŸ·è¡Œï¼ˆå‘¨æ—¥æ·©æ™¨ 4 點)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "無法變更所有者"
@@ -564,7 +684,28 @@ msgstr "跳轉到派生項目"
msgid "GoToYourFork|Fork"
msgstr "跳轉到派生項目"
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -585,12 +726,6 @@ msgstr "沒有檢測到å¥åº·å•é¡Œ"
msgid "HealthCheck|Unhealthy"
msgstr "ä¸è‰¯"
-msgid "Home"
-msgstr "首é "
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "已開始維護"
@@ -612,6 +747,9 @@ msgstr "議題事件 (issue event)"
msgid "Issues"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -674,6 +812,9 @@ msgstr ""
msgid "Merge events"
msgstr "åˆä½µäº‹ä»¶ (merge event)"
+msgid "Merge request"
+msgstr ""
+
msgid "Messages"
msgstr ""
@@ -801,6 +942,18 @@ msgstr ""
msgid "Owner"
msgstr "所有者"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -906,12 +1059,9 @@ msgstr "於階段"
msgid "Preferences"
msgstr ""
-msgid "Profile Settings"
+msgid "Profile"
msgstr ""
-msgid "Project"
-msgstr "專案"
-
msgid "Project '%{project_name}' queued for deletion."
msgstr "項目 '%{project_name}' 已進入刪除隊列。"
@@ -942,12 +1092,6 @@ msgstr "項目導出éˆæŽ¥å·²éŽæœŸã€‚請從項目設置中é‡æ–°ç”Ÿæˆé …目導
msgid "Project export started. A download link will be sent by email."
msgstr "項目導出已開始。下載éˆæŽ¥å°‡é€šéŽé›»å­éƒµä»¶ç™¼é€ã€‚"
-msgid "Project home"
-msgstr "項目首é "
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr "訂閱"
@@ -972,6 +1116,27 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push Rules"
msgstr ""
@@ -990,6 +1155,9 @@ msgstr "分支"
msgid "RefSwitcher|Tags"
msgstr "標籤"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "相關的æ交"
@@ -1044,6 +1212,9 @@ msgstr "ä¿å­˜æµæ°´ç·šè¨ˆåŠƒ"
msgid "Schedule a new pipeline"
msgstr "新建æµæ°´ç·šè¨ˆåŠƒ"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·šè¨ˆåŠƒ"
@@ -1083,6 +1254,12 @@ msgstr "設置密碼"
msgid "Settings"
msgstr ""
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
@@ -1090,6 +1267,102 @@ msgstr[0] "顯示 %d 個事件"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "æºä»£ç¢¼"
@@ -1102,6 +1375,9 @@ msgstr "在 Runner 設置時指定以下 URL:"
msgid "StarProject|Star"
msgstr "星標"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "由此更改 %{new_merge_request}"
@@ -1111,6 +1387,9 @@ msgstr "é‹ä½œ Runner!"
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯/標籤"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "標籤"
@@ -1175,6 +1454,9 @@ msgstr "中ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "è¨ªå• Git 存儲時出ç¾å•é¡Œï¼š"
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "在創建壹個空的存儲庫或導入ç¾æœ‰å­˜å„²åº«ä¹‹å‰ï¼Œæ‚¨å°‡ç„¡æ³•æŽ¨é€ä»£ç¢¼ã€‚"
@@ -1348,9 +1630,15 @@ msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨ä»¥ä¸‹è¨»å†Šä»¤ç‰Œï¼š"
msgid "Use your global notification setting"
msgstr "使用全局通知設置"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "查看開啟的åˆä¸¦è«‹æ±‚"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "內部"
@@ -1423,6 +1711,12 @@ msgstr "在賬號中 %{add_ssh_key_link} 之å‰å°‡ç„¡æ³•é€šéŽ SSH 拉å–或推é
msgid "Your name"
msgstr "您的åå­—"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "天"
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 09c07a83d34..8a14cd01566 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"POT-Creation-Date: 2017-09-27 16:26+0200\n"
+"PO-Revision-Date: 2017-09-27 13:44-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -27,6 +27,9 @@ msgstr[0] "因效能考é‡ï¼Œä¸é¡¯ç¤º %s 個更動 (commit)。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} 在 %{commit_timeago} é€äº¤"
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} æ¬¡å‰ GitLab 會é‡è©¦ã€‚"
@@ -34,7 +37,7 @@ msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block
msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} æ¬¡å‰ GitLab 會在 %{number_of_seconds} 秒後é‡è©¦ã€‚"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "已失敗 %{number_of_failures} / %{maximum_failures} 次,GitLab å°‡ä¸å†è‡ªå‹•é‡è©¦ã€‚請在確èªå•é¡Œè§£æ±ºå¾Œæ‰‹å‹•é‡ç½®å„²å­˜ç©ºé–“資訊。"
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
@@ -47,6 +50,9 @@ msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
+msgid "1st contribution!"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
@@ -63,7 +69,7 @@ msgid "Access to failing storages has been temporarily disabled to allow the mou
msgstr "已暫時åœç”¨å¤±æ•—çš„ Git 儲存空間。當儲存空間æ¢å¾©æ­£å¸¸å¾Œï¼Œè«‹é‡ç½®å„²å­˜ç©ºé–“å¥åº·æŒ‡æ•¸ã€‚"
msgid "Account"
-msgstr ""
+msgstr "帳號"
msgid "Active"
msgstr "啟用"
@@ -89,11 +95,11 @@ msgstr "新增目錄"
msgid "All"
msgstr "全部"
-msgid "Appearances"
+msgid "Appearance"
msgstr ""
msgid "Applications"
-msgstr ""
+msgstr "應用程å¼"
msgid "Archived project! Repository is read-only"
msgstr "此專案已å°å­˜ï¼æª”案庫 (repository) 為唯讀狀態"
@@ -113,10 +119,34 @@ msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證 (access token) 嗎?"
msgid "Are you sure?"
msgstr "確定嗎?"
+msgid "Artifacts"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
-msgid "Authentication log"
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Auto DevOps (Beta)"
+msgstr ""
+
+msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "Auto DevOps documentation"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the"
msgstr ""
msgid "Billing"
@@ -173,6 +203,9 @@ msgstr ""
msgid "Billinglans|Downgrade"
msgstr ""
+msgid "Board"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "分支 (branch) "
@@ -189,6 +222,90 @@ msgstr "切æ›åˆ†æ”¯ (branch)"
msgid "Branches"
msgstr "分支 (branch) "
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project master or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|diverged from upstream"
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
msgid "Browse Directory"
msgstr "ç€è¦½ç›®éŒ„"
@@ -331,9 +448,6 @@ msgstr "é€äº¤è€…為 "
msgid "Compare"
msgstr "比較"
-msgid "Container Registry"
-msgstr ""
-
msgid "Contribution guide"
msgstr "å”作指å—"
@@ -429,7 +543,7 @@ msgid_plural "Deploys"
msgstr[0] "部署"
msgid "Deploy Keys"
-msgstr ""
+msgstr "部署金鑰"
msgid "Description"
msgstr "æè¿°"
@@ -480,6 +594,9 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
msgid "Emails"
+msgstr "é›»å­éƒµä»¶"
+
+msgid "Enable in settings"
msgstr ""
msgid "EventFilterBy|Filter by all"
@@ -509,6 +626,9 @@ msgstr "æ¯æœˆåŸ·è¡Œï¼ˆæ¯æœˆä¸€æ—¥æ·©æ™¨å››é»žï¼‰"
msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯é€±åŸ·è¡Œï¼ˆé€±æ—¥æ·©æ™¨ 四點)"
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "無法變更所有權"
@@ -547,7 +667,7 @@ msgid "From merge request merge until deploy to production"
msgstr "從請求被åˆä½µå¾Œ (merge request merged) 直到部署至營é‹ç’°å¢ƒ"
msgid "GPG Keys"
-msgstr ""
+msgstr "GPG 金鑰"
msgid "Geo Nodes"
msgstr ""
@@ -564,7 +684,28 @@ msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
msgid "GoToYourFork|Fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
-msgid "Group overview"
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
msgid "Health Check"
@@ -585,12 +726,6 @@ msgstr "沒有檢測到å¥åº·å•é¡Œ"
msgid "HealthCheck|Unhealthy"
msgstr "ä¸è‰¯"
-msgid "Home"
-msgstr "首é "
-
-msgid "Hooks"
-msgstr ""
-
msgid "Housekeeping successfully started"
msgstr "已開始維護"
@@ -610,6 +745,9 @@ msgid "Issue events"
msgstr "議題 (issue) 事件"
msgid "Issues"
+msgstr "議題"
+
+msgid "Jobs"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -619,7 +757,7 @@ msgid "LFSStatus|Enabled"
msgstr "啟用"
msgid "Labels"
-msgstr ""
+msgstr "標籤"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -666,22 +804,25 @@ msgid "Median"
msgstr "中ä½æ•¸"
msgid "Members"
-msgstr ""
+msgstr "æˆå“¡"
msgid "Merge Requests"
-msgstr ""
+msgstr "åˆä½µè«‹æ±‚ (merge request)"
msgid "Merge events"
msgstr "åˆä½µ (merge) 事件"
-msgid "Messages"
+msgid "Merge request"
msgstr ""
+msgid "Messages"
+msgstr "公告"
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
msgid "Monitoring"
-msgstr ""
+msgstr "監控"
msgid "More information is available|here"
msgstr "å¥åº·æª¢æŸ¥"
@@ -784,7 +925,7 @@ msgid "NotificationLevel|Watch"
msgstr "關注"
msgid "Notifications"
-msgstr ""
+msgstr "通知"
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
@@ -796,14 +937,26 @@ msgid "Options"
msgstr "é¸é …"
msgid "Overview"
-msgstr ""
+msgstr "總覽"
msgid "Owner"
msgstr "所有權"
-msgid "Password"
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
msgstr ""
+msgid "Password"
+msgstr "密碼"
+
msgid "Pipeline"
msgstr "æµæ°´ç·š (pipeline) "
@@ -904,14 +1057,11 @@ msgid "Pipeline|with stages"
msgstr "於階段"
msgid "Preferences"
-msgstr ""
+msgstr "å好設定"
-msgid "Profile Settings"
+msgid "Profile"
msgstr ""
-msgid "Project"
-msgstr "專案"
-
msgid "Project '%{project_name}' queued for deletion."
msgstr "專案 '%{project_name}' 已加入刪除佇列。"
@@ -942,12 +1092,6 @@ msgstr "專案的匯出連çµå·²å¤±æ•ˆã€‚請到專案設定中產生新的連çµ
msgid "Project export started. A download link will be sent by email."
msgstr "專案導出已開始。完æˆå¾Œä¸‹è¼‰é€£çµæœƒé€åˆ°æ‚¨çš„信箱。"
-msgid "Project home"
-msgstr "專案首é "
-
-msgid "Project overview"
-msgstr ""
-
msgid "ProjectActivityRSS|Subscribe"
msgstr "訂閱"
@@ -972,6 +1116,27 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr "æœå°‹æ‚¨çš„專案"
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr "抱歉,沒有符åˆæœå°‹æ¢ä»¶çš„專案"
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr "此功能需è¦ç€è¦½å™¨æ”¯æ´ localStorage"
+
msgid "Push Rules"
msgstr ""
@@ -990,6 +1155,9 @@ msgstr "分支 (branch) "
msgid "RefSwitcher|Tags"
msgstr "標籤"
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr "相關的更動記錄 (commit) "
@@ -1036,7 +1204,7 @@ msgid "Revert this merge request"
msgstr "還原此åˆä½µè«‹æ±‚ (merge request) "
msgid "SSH Keys"
-msgstr ""
+msgstr "SSH 金鑰"
msgid "Save pipeline schedule"
msgstr "儲存æµæ°´ç·š (pipeline) 排程"
@@ -1044,6 +1212,9 @@ msgstr "儲存æµæ°´ç·š (pipeline) 排程"
msgid "Schedule a new pipeline"
msgstr "建立æµæ°´ç·š (pipeline) 排程"
+msgid "Schedules"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·š (pipeline) 排程"
@@ -1063,7 +1234,7 @@ msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) "
msgid "Service Templates"
-msgstr ""
+msgstr "æœå‹™ç¯„本"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "請先設定密碼,æ‰èƒ½ä½¿ç”¨ %{protocol} 來上傳 (push) 或下載 (pull) 。"
@@ -1081,6 +1252,12 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "設定密碼"
msgid "Settings"
+msgstr "設定"
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
msgstr ""
msgid "Showing %d event"
@@ -1090,6 +1267,102 @@ msgstr[0] "顯示 %d 個事件"
msgid "Snippets"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr ""
+
+msgid "SortOptions|Access level, descending"
+msgstr ""
+
+msgid "SortOptions|Created date"
+msgstr ""
+
+msgid "SortOptions|Due date"
+msgstr ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Less weight"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|More weight"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "SortOptions|Weight"
+msgstr ""
+
msgid "Source code"
msgstr "原始碼"
@@ -1102,6 +1375,9 @@ msgstr "åœ¨å®‰è£ Runner 時指定以下 URL:"
msgid "StarProject|Star"
msgstr "收è—"
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "以這些改動建立一個新的 %{new_merge_request} "
@@ -1111,6 +1387,9 @@ msgstr "å•Ÿå‹• Runner!"
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
+msgid "System Hooks"
+msgstr ""
+
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "標籤"
@@ -1175,6 +1454,9 @@ msgstr "中ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "å­˜å– Git 儲存空間時出ç¾å•é¡Œï¼š"
+msgid "This is the author's first Merge Request to this project. Handle with care."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個ç¾å­˜çš„檔案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•ä¸Šå‚³æ›´æ–° (push) 。"
@@ -1348,9 +1630,15 @@ msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨æ­¤è¨»å†Šæ†‘è­‰ (registration token):"
msgid "Use your global notification setting"
msgstr "使用全域通知設定"
+msgid "View file @ "
+msgstr ""
+
msgid "View open merge request"
msgstr "查看此分支的åˆä½µè«‹æ±‚ (merge request)"
+msgid "View replaced file @ "
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr "內部"
@@ -1370,7 +1658,7 @@ msgid "We don't have enough data to show this stage."
msgstr "因該階段的資料ä¸è¶³è€Œç„¡æ³•é¡¯ç¤ºç›¸é—œè³‡è¨Š"
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "å–消權é™ç”³è«‹"
@@ -1423,6 +1711,12 @@ msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上
msgid "Your name"
msgstr "您的åå­—"
+msgid "Your projects"
+msgstr ""
+
+msgid "commit"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "天"
diff --git a/package.json b/package.json
index feae6ca9748..5aa3ce3f757 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"karma": "karma start config/karma.config.js --single-run",
"karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
"karma-start": "karma start config/karma.config.js",
+ "svg": "node config/svg.config.js",
"webpack": "webpack --config config/webpack.config.js",
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
@@ -28,7 +29,7 @@
"css-loader": "^0.28.0",
"d3": "^3.5.11",
"deckar01-task_list": "^2.0.0",
- "document-register-element": "^1.3.0",
+ "document-register-element": "1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
"eslint-plugin-html": "^2.0.1",
@@ -53,6 +54,7 @@
"react-dev-utils": "^0.5.2",
"select2": "3.5.2-browserify",
"sql.js": "^0.4.0",
+ "svg4everybody": "2.1.9",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
@@ -78,6 +80,7 @@
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
"eslint-plugin-promise": "^3.5.0",
+ "gitlab-svgs": "https://gitlab.com/gitlab-org/gitlab-svgs.git",
"istanbul": "^0.4.5",
"jasmine-core": "^2.6.3",
"jasmine-jquery": "^2.1.1",
diff --git a/qa/Gemfile b/qa/Gemfile
index 5d089a45934..ff29824529f 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -1,5 +1,6 @@
source 'https://rubygems.org'
+gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14'
gem 'rake', '~> 12.0.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 4dd71aa5010..22d12b479cb 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -3,6 +3,7 @@ GEM
specs:
addressable (2.5.0)
public_suffix (~> 2.0, >= 2.0.2)
+ byebug (9.0.6)
capybara (2.12.1)
addressable
mime-types (>= 1.16)
@@ -13,22 +14,27 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- capybara-webkit (1.12.0)
- capybara (>= 2.3.0, < 2.13.0)
- json
childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11)
+ coderay (1.1.1)
diff-lcs (1.3)
ffi (1.9.18)
- json (2.0.3)
launchy (2.4.3)
addressable (~> 2.3)
+ method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
- mini_portile2 (2.1.0)
- nokogiri (1.7.0.1)
- mini_portile2 (~> 2.1.0)
+ mini_portile2 (2.3.0)
+ nokogiri (1.8.1)
+ mini_portile2 (~> 2.3.0)
+ pry (0.10.4)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ pry-byebug (3.4.2)
+ byebug (~> 9.0)
+ pry (~> 0.10)
public_suffix (2.0.5)
rack (2.0.1)
rack-test (0.6.3)
@@ -52,6 +58,7 @@ GEM
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
+ slop (3.6.0)
websocket (1.2.4)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -62,10 +69,10 @@ PLATFORMS
DEPENDENCIES
capybara (~> 2.12.1)
capybara-screenshot (~> 1.0.14)
- capybara-webkit (~> 1.12.0)
+ pry-byebug (~> 3.4.1)
rake (~> 12.0.0)
rspec (~> 3.5)
selenium-webdriver (~> 2.53)
BUNDLED WITH
- 1.14.6
+ 1.15.4
diff --git a/qa/README.md b/qa/README.md
index b6b5a76f1d3..1cfbbdd9d42 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -1,10 +1,10 @@
-## Integration tests for GitLab
+# GitLab QA - Integration tests for GitLab
This directory contains integration tests for GitLab.
-It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+It is part of the [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
-## What GitLab QA is?
+## What is it?
GitLab QA is an integration tests suite for GitLab.
@@ -16,3 +16,38 @@ against any existing instance.
1. When we release a new version of GitLab, we build a Docker images for it.
1. Along with GitLab Docker Images we also build and publish GitLab QA images.
1. GitLab QA project uses these images to execute integration tests.
+
+## How can I use it?
+
+You can use GitLab QA to exercise tests on any live instance! For example, the
+following call would login to a local [GDK] instance and run all specs in
+`qa/specs/features`:
+
+```
+bin/qa Test::Instance http://localhost:3000
+```
+
+### Running specific tests
+
+You can also supply specific tests to run as another parameter. For example, to
+test the EE license specs, you can run:
+
+```
+EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/ee
+```
+
+### Overriding the authenticated user
+
+Unless told otherwise, the QA tests will run as the default `root` user seeded
+by the GDK.
+
+If you need to authenticate as a different user, you can provide the
+`GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables:
+
+```
+GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com
+```
+
+All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables).
+
+[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/
diff --git a/qa/qa.rb b/qa/qa.rb
index db9d8c42fde..eb6f922d0d3 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -31,9 +31,17 @@ module QA
# GitLab instance scenarios.
#
module Gitlab
+ module Group
+ autoload :Create, 'qa/scenario/gitlab/group/create'
+ end
+
module Project
autoload :Create, 'qa/scenario/gitlab/project/create'
end
+
+ module Sandbox
+ autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
+ end
end
end
@@ -55,6 +63,7 @@ module QA
end
module Group
+ autoload :New, 'qa/page/group/new'
autoload :Show, 'qa/page/group/show'
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index b01a4e10f93..baa06b1c75e 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -3,15 +3,8 @@ module QA
module Admin
class Menu < Page::Base
def go_to_license
- within_middle_menu { click_link 'License' }
- end
-
- private
-
- def within_middle_menu
- page.within('.nav-control') do
- yield
- end
+ link = find_link 'License'
+ link.click
end
end
end
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 3690f40dcfe..083d2e1ab16 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -2,19 +2,22 @@ module QA
module Page
module Dashboard
class Groups < Page::Base
- def prepare_test_namespace
- if page.has_content?(Runtime::Namespace.name)
- return click_link(Runtime::Namespace.name)
- end
+ def filter_by_name(name)
+ fill_in 'Filter by name...', with: name
+ end
- click_on 'New group'
+ def has_group?(name)
+ filter_by_name(name)
+
+ page.has_link?(name)
+ end
- fill_in 'group_path', with: Runtime::Namespace.name
- fill_in 'group_description',
- with: "QA test run at #{Runtime::Namespace.time}"
- choose 'Private'
+ def go_to_group(name)
+ click_link name
+ end
- click_button 'Create group'
+ def go_to_new_group
+ click_on 'New group'
end
end
end
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
new file mode 100644
index 00000000000..cb743a7bf11
--- /dev/null
+++ b/qa/qa/page/group/new.rb
@@ -0,0 +1,23 @@
+module QA
+ module Page
+ module Group
+ class New < Page::Base
+ def set_path(path)
+ fill_in 'group_path', with: path
+ end
+
+ def set_description(description)
+ fill_in 'group_description', with: description
+ end
+
+ def set_visibility(visibility)
+ choose visibility
+ end
+
+ def create
+ click_button 'Create group'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 296c311d7c6..6987c1f8f85 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -2,8 +2,24 @@ module QA
module Page
module Group
class Show < Page::Base
+ def go_to_subgroups
+ click_link 'Subgroups'
+ end
+
+ def go_to_subgroup(name)
+ click_link name
+ end
+
+ def has_subgroup?(name)
+ page.has_link?(name)
+ end
+
+ def go_to_new_subgroup
+ click_on 'New Subgroup'
+ end
+
def go_to_new_project
- click_link 'New Project'
+ click_on 'New Project'
end
end
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 7ce4e9009f5..178c5ea6930 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -3,20 +3,19 @@ module QA
module Main
class Menu < Page::Base
def go_to_groups
- within_global_menu { click_link 'Groups' }
+ within_top_menu { click_link 'Groups' }
end
def go_to_projects
- within_global_menu { click_link 'Projects' }
+ within_top_menu { click_link 'Projects' }
end
def go_to_admin_area
- within_user_menu { click_link 'Admin area' }
+ within_top_menu { find('.admin-icon').click }
end
def sign_out
within_user_menu do
- find('.header-user-dropdown-toggle').click
click_link('Sign out')
end
end
@@ -27,17 +26,19 @@ module QA
private
- def within_global_menu
- find('.global-dropdown-toggle').click
-
- page.within('.global-dropdown-menu') do
+ def within_top_menu
+ page.within('.navbar') do
yield
end
end
def within_user_menu
- page.within('.navbar-nav') do
- yield
+ within_top_menu do
+ find('.header-user-dropdown-toggle').click
+
+ page.within('.dropdown-menu-nav') do
+ yield
+ end
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 56a270d8fcc..68d9597c4d2 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -5,8 +5,8 @@ module QA
def choose_repository_clone_http
find('#clone-dropdown').click
- page.within('#clone-dropdown') do
- find('span', text: 'HTTP').click
+ page.within('.clone-options-dropdown') do
+ click_link('HTTP')
end
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index e4910b63a14..996286430b9 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -10,6 +10,10 @@ module QA
def name
'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
end
+
+ def sandbox_name
+ 'gitlab-qa-sandbox'
+ end
end
end
end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 12ceda015f0..60027c89ab1 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -8,7 +8,7 @@ module QA
end
def password
- ENV['GITLAB_PASSWORD'] || 'test1234'
+ ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end
end
end
diff --git a/qa/qa/scenario/gitlab/group/create.rb b/qa/qa/scenario/gitlab/group/create.rb
new file mode 100644
index 00000000000..8e6c7c7ad80
--- /dev/null
+++ b/qa/qa/scenario/gitlab/group/create.rb
@@ -0,0 +1,27 @@
+require 'securerandom'
+
+module QA
+ module Scenario
+ module Gitlab
+ module Group
+ class Create < Scenario::Template
+ attr_writer :path, :description
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def perform
+ Page::Group::New.perform do |group|
+ group.set_path(@path)
+ group.set_description(@description)
+ group.set_visibility('Private')
+ group.create
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
index b860701c304..7b614bfdd94 100644
--- a/qa/qa/scenario/gitlab/project/create.rb
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -12,9 +12,23 @@ module QA
end
def perform
- Page::Main::Menu.act { go_to_groups }
- Page::Dashboard::Groups.act { prepare_test_namespace }
- Page::Group::Show.act { go_to_new_project }
+ Scenario::Gitlab::Sandbox::Prepare.perform
+
+ Page::Group::Show.perform do |page|
+ page.go_to_subgroups
+
+ if page.has_subgroup?(Runtime::Namespace.name)
+ page.go_to_subgroup(Runtime::Namespace.name)
+ else
+ page.go_to_new_subgroup
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.name
+ end
+ end
+
+ page.go_to_new_project
+ end
Page::Project::New.perform do |page|
page.choose_test_namespace
diff --git a/qa/qa/scenario/gitlab/sandbox/prepare.rb b/qa/qa/scenario/gitlab/sandbox/prepare.rb
new file mode 100644
index 00000000000..990de456e20
--- /dev/null
+++ b/qa/qa/scenario/gitlab/sandbox/prepare.rb
@@ -0,0 +1,28 @@
+module QA
+ module Scenario
+ module Gitlab
+ module Sandbox
+ # Ensure we're in our sandbox namespace, either by navigating to it or
+ # by creating it if it doesn't yet exist
+ class Prepare < Scenario::Template
+ def perform
+ Page::Main::Menu.act { go_to_groups }
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(Runtime::Namespace.sandbox_name)
+ page.go_to_group(Runtime::Namespace.sandbox_name)
+ else
+ page.go_to_new_group
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.sandbox_name
+ group.description = 'QA sandbox'
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 4dfdd6cd93c..79c681168cc 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -43,8 +43,7 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => {
- 'binary' => '/usr/bin/google-chrome-stable',
- 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024]
+ 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
}
)
diff --git a/rubocop/cop/rspec/verbose_include_metadata.rb b/rubocop/cop/rspec/verbose_include_metadata.rb
new file mode 100644
index 00000000000..58390622d60
--- /dev/null
+++ b/rubocop/cop/rspec/verbose_include_metadata.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # Checks for verbose include metadata used in the specs.
+ #
+ # @example
+ # # bad
+ # describe MyClass, js: true do
+ # end
+ #
+ # # good
+ # describe MyClass, :js do
+ # end
+ class VerboseIncludeMetadata < Cop
+ MSG = 'Use `%s` instead of `%s`.'
+
+ SELECTORS = %i[describe context feature example_group it specify example scenario its].freeze
+
+ def_node_matcher :include_metadata, <<-PATTERN
+ (send {(const nil :RSpec) nil} {#{SELECTORS.map(&:inspect).join(' ')}}
+ !const
+ ...
+ (hash $...))
+ PATTERN
+
+ def_node_matcher :invalid_metadata?, <<-PATTERN
+ (pair
+ (sym $...)
+ (true))
+ PATTERN
+
+ def on_send(node)
+ invalid_metadata_matches(node) do |match|
+ add_offense(node, :expression, format(MSG, good(match), bad(match)))
+ end
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ invalid_metadata_matches(node) do |match|
+ corrector.replace(match.loc.expression, good(match))
+ end
+ end
+ end
+
+ private
+
+ def invalid_metadata_matches(node)
+ include_metadata(node) do |matches|
+ matches.select(&method(:invalid_metadata?)).each do |match|
+ yield match
+ end
+ end
+ end
+
+ def bad(match)
+ "#{metadata_key(match)}: true"
+ end
+
+ def good(match)
+ ":#{metadata_key(match)}"
+ end
+
+ def metadata_key(match)
+ match.children[0].source
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 1b6e8991a17..1df23899efb 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -21,3 +21,4 @@ require_relative 'cop/migration/reversible_add_column_with_default'
require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
require_relative 'cop/rspec/single_line_hook'
+require_relative 'cop/rspec/verbose_include_metadata'
diff --git a/scripts/lint-changelog-yaml b/scripts/lint-changelog-yaml
new file mode 100755
index 00000000000..cce5f1c7667
--- /dev/null
+++ b/scripts/lint-changelog-yaml
@@ -0,0 +1,22 @@
+#!/usr/bin/env ruby
+
+require 'yaml'
+
+invalid_changelogs = Dir['changelogs/**/*'].reject do |changelog|
+ next true if changelog =~ /(archive\.md|unreleased(-ee)?)$/
+ next false unless changelog.end_with?('.yml')
+
+ begin
+ YAML.load_file(changelog)
+ rescue
+ end
+end
+
+if invalid_changelogs.any?
+ puts "Invalid changelogs found!\n"
+ puts invalid_changelogs.sort
+ exit 1
+else
+ puts "All changelogs are valid YAML.\n"
+ exit 0
+end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 54c1ef3dfdd..e5242fee32b 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -3,15 +3,19 @@
cd "$(dirname "$0")/.."
# Use long options (e.g. --header instead of -H) for curl examples in documentation.
-grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
+echo 'Checking for curl short options...'
+grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ >/dev/null 2>&1
if [ $? == 0 ]
then
- echo '✖ ERROR: Short options should not be used in documentation!' >&2
+ echo '✖ ERROR: Short options for curl should not be used in documentation!
+ Use long options (e.g., --header instead of -H):' >&2
+ grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
exit 1
fi
# Ensure that the CHANGELOG.md does not contain duplicate versions
DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
+echo 'Checking for CHANGELOG.md duplicate entries...'
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
@@ -19,5 +23,15 @@ then
exit 1
fi
+# Make sure no files in doc/ are executable
+EXEC_PERM_COUNT=$(find doc/ app/ -type f -perm 755 | wc -l)
+echo 'Checking for executable permissions...'
+if [ "${EXEC_PERM_COUNT}" -ne 0 ]
+then
+ echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2
+ find doc/ app/ -type f -perm 755
+ exit 1
+fi
+
echo "✔ Linting passed"
exit 0
diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh
new file mode 100644
index 00000000000..5de2b35571d
--- /dev/null
+++ b/scripts/schema_changed.sh
@@ -0,0 +1,10 @@
+function schema_changed() {
+ if [[ ! -z `git diff --name-only -- db/schema.rb` ]]; then
+ echo "db/schema.rb after rake db:migrate:reset is different from one in the repository"
+ exit 1
+ else
+ echo "db/schema.rb after rake db:migrate:reset matches one in the repository"
+ fi
+}
+
+schema_changed
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 295b6f132c1..aeefb2bc96f 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -13,7 +13,8 @@ tasks = [
%w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec],
%w[scripts/lint-conflicts.sh],
- %w[bundle exec rake gettext:lint]
+ %w[bundle exec rake gettext:lint],
+ %w[scripts/lint-changelog-yaml]
]
failed_tasks = tasks.reduce({}) do |failures, task|
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index 44f832ed3e6..d3a9f5ff4ea 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -3,13 +3,6 @@
require 'gitlab'
#
-# Give the remote branch a different name than the current one
-# in order to avoid conflicts
-#
-@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee"
-GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
-
-#
# Configure credentials to be used with gitlab gem
#
Gitlab.configure do |config|
@@ -18,6 +11,26 @@ Gitlab.configure do |config|
end
#
+# The remote docs project
+#
+GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
+
+#
+# Truncate the remote docs branch name if it's more than 63 characters
+# otherwise we hit the filesystem limit and the directory name where
+# NGINX serves the site won't match the branch name.
+#
+def docs_branch
+ # The maximum string length a file can have on a filesystem (ext4)
+ # is 63 characters. Let's use something smaller to be 100% sure.
+ max = 42
+ # Prefix the remote branch with 'preview-' in order to avoid
+ # name conflicts in the rare case the branch name already
+ # exists in the docs repo and truncate to max length.
+ "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
+end
+
+#
# Dummy way to find out in which repo we are, CE or EE
#
def ee?
@@ -28,18 +41,18 @@ end
# Create a remote branch in gitlab-docs
#
def create_remote_branch
- Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master')
- puts "Remote branch '#{@docs_branch}' created"
+ Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master')
+ puts "Remote branch '#{docs_branch}' created"
rescue Gitlab::Error::BadRequest
- puts "Remote branch '#{@docs_branch}' already exists"
+ puts "Remote branch '#{docs_branch}' already exists"
end
#
# Remove a remote branch in gitlab-docs
#
def remove_remote_branch
- Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch)
- puts "Remote branch '#{@docs_branch}' deleted"
+ Gitlab.delete_branch(GITLAB_DOCS_REPO, docs_branch)
+ puts "Remote branch '#{docs_branch}' deleted"
end
#
@@ -50,11 +63,11 @@ def trigger_pipeline
param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
# The review app URL
- app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
+ app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
# Create the pipeline
puts "=> Triggering a pipeline..."
- pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
+ pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["CI_JOB_TOKEN"], docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
puts "=> Pipeline created:"
puts ""
@@ -77,4 +90,8 @@ when 'deploy'
trigger_pipeline
when 'cleanup'
remove_remote_branch
+else
+ puts "Please provide a valid option:
+ deploy - Creates the remote branch and triggers a pipeline
+ cleanup - Deletes the remote branch and stops the Review App"
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index aadd3317875..25fe547ff37 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Admin::UsersController do
let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:admin) { create(:admin) }
before do
sign_in(admin)
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index dfa06c78d46..5163099cd98 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -45,6 +45,17 @@ describe Boards::IssuesController do
expect(parsed_response.length).to eq 2
expect(development.issues.map(&:relative_position)).not_to include(nil)
end
+
+ it 'avoids N+1 database queries' do
+ create(:labeled_issue, project: project, labels: [development])
+ control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count
+
+ # 25 issues is bigger than the page size
+ # the relative position will ignore the `#make_sure_position_set` queries
+ create_list(:labeled_issue, 25, project: project, labels: [development], assignees: [johndoe], relative_position: 1)
+
+ expect { list_issues(user: user, board: board, list: list2) }.not_to exceed_query_limit(control_count)
+ end
end
context 'with invalid list id' do
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index c8c6b9f41bf..9df4ebf2fa0 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -57,7 +57,7 @@ describe Dashboard::TodosController do
expect(response).to redirect_to(dashboard_todos_path(page: last_page))
end
- it 'redirects to correspondent page' do
+ it 'goes to the correct page' do
get :index, page: last_page
expect(assigns(:todos).current_page).to eq(last_page)
@@ -70,6 +70,30 @@ describe Dashboard::TodosController do
expect(response).to redirect_to(dashboard_todos_path(page: last_page))
end
+
+ context 'when providing no filters' do
+ it 'does not perform a query to get the page count, but gets that from the user' do
+ allow(controller).to receive(:current_user).and_return(user)
+
+ expect(user).to receive(:todos_pending_count).and_call_original
+
+ get :index, page: (last_page + 1).to_param, sort: :created_asc
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page, sort: :created_asc))
+ end
+ end
+
+ context 'when providing filters' do
+ it 'performs a query to get the correct page count' do
+ allow(controller).to receive(:current_user).and_return(user)
+
+ expect(user).not_to receive(:todos_pending_count)
+
+ get :index, page: (last_page + 1).to_param, project_id: project.id
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page, project_id: project.id))
+ end
+ end
end
end
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
new file mode 100644
index 00000000000..80d553f0f34
--- /dev/null
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe GoogleApi::AuthorizationsController do
+ describe 'GET|POST #callback' do
+ let(:user) { create(:user) }
+ let(:token) { 'token' }
+ let(:expires_at) { 1.hour.since.strftime('%s') }
+
+ subject { get :callback, code: 'xxx', state: @state }
+
+ before do
+ sign_in(user)
+
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:get_token).and_return([token, expires_at])
+ end
+
+ it 'sets token and expires_at in session' do
+ subject
+
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
+ .to eq(token)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
+ .to eq(expires_at)
+ end
+
+ context 'when redirect uri key is stored in state' do
+ set(:project) { create(:project) }
+ let(:redirect_uri) { project_clusters_url(project).to_s }
+
+ before do
+ @state = GoogleApi::CloudPlatform::Client
+ .new_session_key_for_redirect_uri do |key|
+ session[key] = redirect_uri
+ end
+ end
+
+ it 'redirects to the URL stored in state param' do
+ expect(subject).to redirect_to(redirect_uri)
+ end
+ end
+
+ context 'when redirection url is not stored in state' do
+ it 'redirects to root_path' do
+ expect(subject).to redirect_to(root_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index cc389e554ad..9e9cf4f2c1f 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -10,6 +10,7 @@ describe HealthController do
before do
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip])
+ stub_storage_settings({}) # Hide the broken storage
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
new file mode 100644
index 00000000000..ecf14aad54f
--- /dev/null
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Profiles::EmailsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#create' do
+ let(:email_params) { { email: "add_email@example.com" } }
+
+ it 'sends an email confirmation' do
+ expect { post(:create, { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
+ expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
+ end
+ end
+
+ describe '#resend_confirmation_instructions' do
+ let(:email_params) { { email: "add_email@example.com" } }
+
+ it 'resends an email confirmation' do
+ email = user.emails.create(email: 'add_email@example.com')
+
+ expect { put(:resend_confirmation_instructions, { id: email }) }.to change { ActionMailer::Base.deliveries.size }
+ expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
+ end
+
+ it 'unable to resend an email confirmation' do
+ expect { put(:resend_confirmation_instructions, { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size }
+ end
+ end
+end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index b52b63e05a4..ce5040ff02b 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -15,6 +15,20 @@ describe ProfilesController do
expect(user.unconfirmed_email).to eq('john@gmail.com')
end
+ it "allows an email update without confirmation if existing verified email" do
+ user = create(:user)
+ create(:email, :confirmed, user: user, email: 'john@gmail.com')
+ sign_in(user)
+
+ put :update,
+ user: { email: "john@gmail.com", name: "John" }
+
+ user.reload
+
+ expect(response.status).to eq(302)
+ expect(user.unconfirmed_email).to eq nil
+ end
+
it "ignores an email update from a user with an external email address" do
stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true)
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 2bd8f8e2bfc..6defb9e15e6 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Projects::ArtifactsController do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :repository, :public) }
let(:pipeline) do
create(:ci_pipeline,
@@ -15,7 +15,7 @@ describe Projects::ArtifactsController do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
@@ -47,19 +47,67 @@ describe Projects::ArtifactsController do
end
describe 'GET file' do
- context 'when the file exists' do
- it 'renders the file view' do
- get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
- expect(response).to render_template('projects/artifacts/file')
+ context 'when the file is served by GitLab Pages' do
+ before do
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'when the file exists' do
+ it 'renders the file view' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
end
end
- context 'when the file does not exist' do
- it 'responds Not Found' do
- get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+ context 'when the file is served through Rails' do
+ context 'when the file exists' do
+ it 'renders the file view' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
- expect(response).to be_not_found
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/artifacts/file')
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project, :repository, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ before do
+ private_project.add_developer(user)
+
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ it 'does not redirect the request' do
+ get :file, namespace_id: private_project.namespace, project_id: private_project, job_id: job, path: 'ci_artifacts.txt'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/artifacts/file')
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 64b9af7b845..fb76b7fdf38 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe Projects::BlobController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
describe "GET show" do
@@ -226,9 +228,8 @@ describe Projects::BlobController do
end
context 'when user has forked project' do
- let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
- let!(:forked_project) { forked_project_link.forked_to_project }
- let(:guest) { forked_project.owner }
+ let!(:forked_project) { fork_project(project, guest, namespace: guest.namespace, repository: true) }
+ let(:guest) { create(:user) }
before do
sign_in(guest)
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 745d051a5c1..5e0b57e9b2e 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -367,5 +367,20 @@ describe Projects::BranchesController do
expect(parsed_response.first).to eq 'master'
end
end
+
+ context 'when branch contains an invalid UTF-8 sequence' do
+ before do
+ project.repository.create_branch("wrong-\xE5-utf8-sequence")
+ end
+
+ it 'return with a status 200' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :html
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
new file mode 100644
index 00000000000..7985028d73b
--- /dev/null
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -0,0 +1,308 @@
+require 'spec_helper'
+
+describe Projects::ClustersController do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ let(:role) { :master }
+
+ before do
+ project.team << [user, role]
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ subject do
+ get :index, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when cluster is already created' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ it 'redirects to show a cluster' do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ end
+ end
+
+ context 'when we do not have cluster' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(new_project_cluster_path(project))
+ end
+ end
+ end
+
+ describe 'GET login' do
+ render_views
+
+ subject do
+ get :login, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when we do have omniauth configured' do
+ it 'shows login button' do
+ subject
+
+ expect(response.body).to include('auth_buttons/signin_with_google')
+ end
+ end
+
+ context 'when we do not have omniauth configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'shows notice message' do
+ subject
+
+ expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ end
+ end
+ end
+
+ shared_examples 'requires to login' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(login_project_clusters_path(project))
+ end
+ end
+
+ describe 'GET new' do
+ render_views
+
+ subject do
+ get :new, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when logged' do
+ before do
+ make_logged_in
+ end
+
+ it 'shows a creation form' do
+ subject
+
+ expect(response.body).to include('Create cluster')
+ end
+ end
+
+ context 'when not logged' do
+ it_behaves_like 'requires to login'
+ end
+ end
+
+ describe 'POST create' do
+ subject do
+ post :create, params.merge(namespace_id: project.namespace,
+ project_id: project)
+ end
+
+ context 'when not logged' do
+ let(:params) { {} }
+
+ it_behaves_like 'requires to login'
+ end
+
+ context 'when logged in' do
+ before do
+ make_logged_in
+ end
+
+ context 'when all required parameters are set' do
+ let(:params) do
+ {
+ cluster: {
+ gcp_cluster_name: 'new-cluster',
+ gcp_project_id: '111'
+ }
+ }
+ end
+
+ before do
+ expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ end
+
+ it 'creates a new cluster' do
+ expect { subject }.to change { Gcp::Cluster.count }
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
+ end
+
+ context 'when not all required parameters are set' do
+ render_views
+
+ let(:params) do
+ {
+ cluster: {
+ project_namespace: 'some namespace'
+ }
+ }
+ end
+
+ it 'shows an error message' do
+ expect { subject }.not_to change { Gcp::Cluster.count }
+
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
+
+ describe 'GET status' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster,
+ format: :json
+ end
+
+ it "responds with matching schema" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'GET show' do
+ render_views
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "allows to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Save")
+ end
+
+ it "allows remove integration" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Remove integration")
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to access page" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'PUT update' do
+ render_views
+
+ let(:service) { project.build_kubernetes_service }
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
+ let(:params) { {} }
+
+ subject do
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
+ end
+
+ context 'when logged as master' do
+ context 'when valid params are used' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
+
+ it "redirects back to show page" do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when invalid params are used' do
+ let(:params) do
+ {
+ cluster: { project_namespace: 'my Namespace 321321321 #' }
+ }
+ end
+
+ it "rejects changes" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'delete update' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "redirects back to clusters list" do
+ subject
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to destroy cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ def make_logged_in
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
+ end
+
+ def in_hour
+ Time.now + 1.hour
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5d9403c23ac..053bd73fee3 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -207,162 +207,6 @@ describe Projects::IssuesController do
end
end
- describe 'PUT #update' do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- it_behaves_like 'update invalid issuable', Issue
-
- context 'changing the assignee' do
- it 'limits the attributes exposed on the assignee' do
- assignee = create(:user)
- project.add_developer(assignee)
-
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: issue.iid,
- issue: { assignee_ids: [assignee.id] },
- format: :json
- body = JSON.parse(response.body)
-
- expect(body['assignees'].first.keys)
- .to match_array(%w(id name username avatar_url state web_url))
- end
- end
-
- context 'Akismet is enabled' do
- let(:project) { create(:project_empty_repo, :public) }
-
- before do
- stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- end
-
- context 'when an issue is not identified as spam' do
- before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
- end
-
- it 'normally updates the issue' do
- expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
- end
- end
-
- context 'when an issue is identified as spam' do
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
- context 'when captcha is not verified' do
- def update_spam_issue
- update_issue(title: 'Spam Title', description: 'Spam lives here')
- end
-
- before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
- end
-
- it 'rejects an issue recognized as a spam' do
- expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true)
- expect { update_spam_issue }.not_to change { issue.reload.title }
- end
-
- it 'rejects an issue recognized as a spam when recaptcha disabled' do
- stub_application_setting(recaptcha_enabled: false)
-
- expect { update_spam_issue }.not_to change { issue.reload.title }
- end
-
- it 'creates a spam log' do
- update_spam_issue
-
- spam_logs = SpamLog.all
-
- expect(spam_logs.count).to eq(1)
- expect(spam_logs.first.title).to eq('Spam Title')
- expect(spam_logs.first.recaptcha_verified).to be_falsey
- end
-
- context 'as HTML' do
- it 'renders verify template' do
- update_spam_issue
-
- expect(response).to render_template(:verify)
- end
- end
-
- context 'as JSON' do
- before do
- update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json)
- end
-
- it 'renders json errors' do
- expect(json_response)
- .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
- end
-
- it 'returns 422 status' do
- expect(response).to have_http_status(422)
- end
- end
- end
-
- context 'when captcha is verified' do
- let(:spammy_title) { 'Whatever' }
- let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
-
- def update_verified_issue
- update_issue({ title: spammy_title },
- { spam_log_id: spam_logs.last.id,
- recaptcha_verification: true })
- end
-
- before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha)
- .and_return(true)
- end
-
- it 'redirect to issue page' do
- update_verified_issue
-
- expect(response)
- .to redirect_to(project_issue_path(project, issue))
- end
-
- it 'accepts an issue after recaptcha is verified' do
- expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
- end
-
- it 'marks spam log as recaptcha_verified' do
- expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
- end
-
- it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
- spam_log = create(:spam_log)
-
- expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }
- .not_to change { SpamLog.last.recaptcha_verified }
- end
- end
- end
-
- def update_issue(issue_params = {}, additional_params = {})
- params = {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: issue.iid,
- issue: issue_params
- }.merge(additional_params)
-
- put :update, params
- end
- end
- end
-
describe 'POST #move' do
before do
sign_in(user)
@@ -533,6 +377,146 @@ describe Projects::IssuesController do
end
end
+ describe 'PUT #update' do
+ def update_issue(issue_params: {}, additional_params: {}, id: nil)
+ id ||= issue.iid
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id,
+ issue: { title: 'New title' }.merge(issue_params),
+ format: :json
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
+ def go(id:)
+ update_issue(id: id)
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'restricted action', success: 200
+ it_behaves_like 'update invalid issuable', Issue
+
+ context 'changing the assignee' do
+ it 'limits the attributes exposed on the assignee' do
+ assignee = create(:user)
+ project.add_developer(assignee)
+
+ update_issue(issue_params: { assignee_ids: [assignee.id] })
+
+ body = JSON.parse(response.body)
+
+ expect(body['assignees'].first.keys)
+ .to match_array(%w(id name username avatar_url state web_url))
+ end
+ end
+
+ context 'Akismet is enabled' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when an issue is not identified as spam' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+ end
+
+ it 'normally updates the issue' do
+ expect { update_issue(issue_params: { title: 'Foo' }) }.to change { issue.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when an issue is identified as spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ end
+
+ context 'when captcha is not verified' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ end
+
+ it 'rejects an issue recognized as a spam' do
+ expect { update_issue }.not_to change { issue.reload.title }
+ end
+
+ it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { update_issue }.not_to change { issue.reload.title }
+ end
+
+ it 'creates a spam log' do
+ update_issue(issue_params: { title: 'Spam title' })
+
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs.first.title).to eq('Spam title')
+ expect(spam_logs.first.recaptcha_verified).to be_falsey
+ end
+
+ it 'renders json errors' do
+ update_issue
+
+ expect(json_response)
+ .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
+ end
+
+ it 'returns 422 status' do
+ update_issue
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let(:spammy_title) { 'Whatever' }
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
+
+ def update_verified_issue
+ update_issue(
+ issue_params: { title: spammy_title },
+ additional_params: { spam_log_id: spam_logs.last.id, recaptcha_verification: true })
+ end
+
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha)
+ .and_return(true)
+ end
+
+ it 'returns 200 status' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ end
+
+ it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+ spam_log = create(:spam_log)
+
+ expect { update_issue(issue_params: { spam_log_id: spam_log.id, recaptcha_verification: true }) }
+ .not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
+ end
+ end
+ end
+
describe 'GET #show' do
it_behaves_like 'restricted action', success: 200
@@ -573,29 +557,6 @@ describe Projects::IssuesController do
end
end
end
-
- describe 'GET #edit' do
- it_behaves_like 'restricted action', success: 200
-
- def go(id:)
- get :edit,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: id
- end
- end
-
- describe 'PUT #update' do
- it_behaves_like 'restricted action', success: 302
-
- def go(id:)
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: id,
- issue: { title: 'New title' }
- end
- end
end
describe 'POST #create' do
@@ -900,5 +861,37 @@ describe Projects::IssuesController do
expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end
+
+ context 'with cross-reference system note', :request_store do
+ let(:new_issue) { create(:issue) }
+ let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
+
+ before do
+ create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ end
+
+ it 'filters notes that the user should not see' do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ it 'does not result in N+1 queries' do
+ # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ RequestStore.clear!
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+ end.count
+
+ RequestStore.clear!
+
+ create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+
+ expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index fdd7e6f173f..d01339a0b88 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -216,7 +216,7 @@ describe Projects::JobsController 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 "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index fad2c8f3ab7..7260350d5fb 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequests::DiffsController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -37,12 +39,12 @@ describe Projects::MergeRequests::DiffsController do
render_views
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
+ project.add_developer(user)
+
merge_request.reload
go
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 6775012bab5..707e7c32283 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -96,18 +98,6 @@ describe Projects::MergeRequestsController do
expect(response).to match_response_schema('entities/merge_request')
end
end
-
- context 'number of queries', :request_store do
- it 'verifies number of queries' do
- # pre-create objects
- merge_request
-
- recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
-
- expect(recorded.count).to be_within(5).of(30)
- expect(recorded.cached_count).to eq(0)
- end
- end
end
describe "as diff" do
@@ -216,14 +206,11 @@ describe Projects::MergeRequestsController do
context 'there is no source project' do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- merge_request.reload
- fork_project.destroy
+ forked_project.destroy
end
it 'closes MR without errors' do
@@ -611,21 +598,16 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do
- let!(:forked) { create(:project, :repository) }
+ let!(:forked) { fork_project(project, user, repository: true) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
let(:admin) { create(:admin) }
let(:merge_request) do
- create(:forked_project_link, forked_to_project: forked,
- forked_from_project: project)
-
create(:merge_request, source_project: forked, target_project: project)
end
before do
- forked.team << [user, :master]
-
get :ci_environments_status,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
@@ -658,7 +640,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 "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 6ffe41b8608..135fd6449ff 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::NotesController do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -120,6 +122,40 @@ describe Projects::NotesController do
expect(note_json[:diff_discussion_html]).to be_nil
end
end
+
+ context 'with cross-reference system note', :request_store do
+ let(:new_issue) { create(:issue) }
+ let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
+
+ before do
+ note
+ create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ end
+
+ it 'filters notes that the user should not see' do
+ get :index, request_params
+
+ expect(parsed_response[:notes].count).to eq(1)
+ expect(note_json[:id]).to eq(note.id)
+ end
+
+ it 'does not result in N+1 queries' do
+ # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
+ get :index, request_params
+
+ RequestStore.clear!
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get :index, request_params
+ end.count
+
+ RequestStore.clear!
+
+ create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+
+ expect { get :index, request_params }.not_to exceed_query_limit(control_count)
+ end
+ end
end
describe 'POST create' do
@@ -176,18 +212,16 @@ describe Projects::NotesController do
context 'when creating a commit comment from an MR fork' do
let(:project) { create(:project, :repository) }
- let(:fork_project) do
- create(:project, :repository).tap do |fork|
- create(:forked_project_link, forked_to_project: fork, forked_from_project: project)
- end
+ let(:forked_project) do
+ fork_project(project, nil, repository: true)
end
let(:merge_request) do
- create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master')
+ create(:merge_request, source_project: forked_project, target_project: project, source_branch: 'feature', target_branch: 'master')
end
let(:existing_comment) do
- create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first)
+ create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first)
end
def post_create(extra_params = {})
@@ -197,7 +231,7 @@ describe Projects::NotesController do
project_id: project,
target_type: 'merge_request',
target_id: merge_request.id,
- note_project_id: fork_project.id,
+ note_project_id: forked_project.id,
in_reply_to_discussion_id: existing_comment.discussion_id
}.merge(extra_params)
end
@@ -219,16 +253,66 @@ describe Projects::NotesController do
end
context 'when the user has access to the fork' do
- let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) }
+ let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) }
before do
- fork_project.add_developer(user)
+ forked_project.add_developer(user)
existing_comment
end
it 'creates the note' do
- expect { post_create }.to change { fork_project.notes.count }.by(1)
+ expect { post_create }.to change { forked_project.notes.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a noteable is not found' do
+ it 'returns 404 status' do
+ request_params[:note][:noteable_id] = 9999
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when a user is a team member' do
+ it 'returns 302 status for html' do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it 'returns 200 status for json' do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'creates a new note' do
+ expect { post :create, request_params }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ project.project_member(user).destroy
+ end
+
+ it 'returns 404 status' do
+ post :create, request_params
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not create a new note' do
+ expect { post :create, request_params }.not_to change { Note.count }
end
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index f9d77c7ad03..167e80ed9cd 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -142,7 +142,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 "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
end
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
new file mode 100644
index 00000000000..ee46ad00947
--- /dev/null
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::PipelinesSettingsController do
+ set(:user) { create(:user) }
+ set(:project_auto_devops) { create(:project_auto_devops) }
+ let(:project) { project_auto_devops.project }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ describe 'PATCH update' do
+ before do
+ patch :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ project: {
+ auto_devops_attributes: params
+ }
+ end
+
+ context 'when updating the auto_devops settings' do
+ let(:params) { { enabled: '', domain: 'mepmep.md' } }
+
+ it 'redirects to the settings page' do
+ expect(response).to have_http_status(302)
+ expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.")
+ end
+
+ context 'following the instance default' do
+ let(:params) { { enabled: '' } }
+
+ it 'allows enabled to be set to nil' do
+ project_auto_devops.reload
+
+ expect(project_auto_devops.enabled).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index 2805968dcd9..5d9d5351687 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do
expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
expect(ContainerRepository.first).to be_root_repository
end
+
+ it 'json has a list of projects' do
+ go_to_index(format: :json)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/repositories')
+ end
end
context 'when there are no tags for this repository' do
@@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do
it 'does not ensure root container repository' do
expect { go_to_index }.not_to change { ContainerRepository.all.count }
end
+
+ it 'responds with json if asked' do
+ go_to_index(format: :json)
+
+ expect(response).to have_http_status(:ok)
+ expect(json_response).to be_kind_of(Array)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ context 'when root container repository exists' do
+ let!(:repository) do
+ create(:container_repository, :root, project: project)
+ end
+
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
+ it 'deletes a repository' do
+ expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1)
+
+ expect(response).to have_http_status(:no_content)
end
end
end
@@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do
end
end
- def go_to_index
+ def go_to_index(format: :html)
get :index, namespace_id: project.namespace,
- project_id: project
+ project_id: project,
+ format: format
+ end
+
+ def delete_repository(repository)
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: repository,
+ format: :json
end
end
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index f4af3587d23..bb702ebeb23 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do
let(:user) { create(:user) }
let(:project) { create(:project, :private) }
+ let(:repository) do
+ create(:container_repository, name: 'image', project: project)
+ end
+
before do
sign_in(user)
stub_container_registry_config(enabled: true)
end
- context 'when user has access to registry' do
+ describe 'GET index' do
+ let(:tags) do
+ Array.new(40) { |i| "tag#{i}" }
+ end
+
before do
- project.add_developer(user)
+ stub_container_registry_tags(repository: /image/, tags: tags)
end
- describe 'POST destroy' do
+ context 'when user can control the registry' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ context 'when user can read the registry' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ context 'when user does not have access to registry' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ private
+
+ def get_tags
+ get :index, namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ format: :json
+ end
+ end
+
+ describe 'POST destroy' do
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
+
context 'when there is matching tag present' do
before do
- stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.])
- end
-
- let(:repository) do
- create(:container_repository, name: 'image', project: project)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end
it 'makes it possible to delete regular tag' do
@@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do
end
end
end
- end
- def destroy_tag(name)
- post :destroy, namespace_id: project.namespace,
- project_id: project,
- repository_id: repository,
- id: name
+ private
+
+ def destroy_tag(name)
+ post :destroy, namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ id: name,
+ format: :json
+ end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 4459e227fb3..0544afe31ed 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1,6 +1,8 @@
require('spec_helper')
describe ProjectsController do
+ include ProjectForksHelper
+
let(:project) { create(:project) }
let(:public_project) { create(:project, :public) }
let(:user) { create(:user) }
@@ -139,8 +141,9 @@ describe ProjectsController do
end
end
- context 'when the storage is not available', broken_storage: true do
- let(:project) { create(:project, :broken_storage) }
+ context 'when the storage is not available', :broken_storage do
+ set(:project) { create(:project, :broken_storage) }
+
before do
project.add_developer(user)
sign_in(user)
@@ -289,6 +292,24 @@ describe ProjectsController do
end
end
+ it 'updates Fast Forward Merge attributes' do
+ controller.instance_variable_set(:@project, project)
+
+ params = {
+ merge_method: :ff
+ }
+
+ put :update,
+ namespace_id: project.namespace,
+ id: project.id,
+ project: params
+
+ expect(response).to have_http_status(302)
+ params.each do |param, value|
+ expect(project.public_send(param)).to eq(value)
+ end
+ end
+
def update_project(**parameters)
put :update,
namespace_id: project.namespace.path,
@@ -358,10 +379,10 @@ describe ProjectsController do
context "when the project is forked" do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
let(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -369,7 +390,7 @@ describe ProjectsController do
project.merge_requests << merge_request
sign_in(admin)
- delete :destroy, namespace_id: fork_project.namespace, id: fork_project
+ delete :destroy, namespace_id: forked_project.namespace, id: forked_project
expect(merge_request.reload.state).to eq('closed')
end
@@ -436,18 +457,14 @@ describe ProjectsController do
end
context 'with forked project' do
- let(:project_fork) { create(:project, :repository, namespace: user.namespace) }
-
- before do
- create(:forked_project_link, forked_to_project: project_fork)
- end
+ let(:forked_project) { fork_project(create(:project, :public), user) }
it 'removes fork from project' do
delete(:remove_fork,
- namespace_id: project_fork.namespace.to_param,
- id: project_fork.to_param, format: :js)
+ namespace_id: forked_project.namespace.to_param,
+ id: forked_project.to_param, format: :js)
- expect(project_fork.forked?).to be_falsey
+ expect(forked_project.reload.forked?).to be_falsey
expect(flash[:notice]).to eq('The fork relationship has been removed.')
expect(response).to render_template(:remove_fork)
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 5a4ab39ab86..1d3ddfbd220 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -76,12 +76,68 @@ describe RegistrationsController do
sign_in(user)
end
- it 'schedules the user for destruction' do
- expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {})
+ def expect_failure(message)
+ expect(flash[:alert]).to eq(message)
+ expect(response.status).to eq(303)
+ expect(response).to redirect_to profile_account_path
+ end
+
+ def expect_password_failure
+ expect_failure('Invalid password')
+ end
+
+ def expect_username_failure
+ expect_failure('Invalid username')
+ end
+
+ def expect_success
+ expect(flash[:notice]).to eq 'Account scheduled for removal.'
+ expect(response.status).to eq(303)
+ expect(response).to redirect_to new_user_session_path
+ end
- post(:destroy)
+ context 'user requires password confirmation' do
+ it 'fails if password confirmation is not provided' do
+ post :destroy
- expect(response.status).to eq(302)
+ expect_password_failure
+ end
+
+ it 'fails if password confirmation is wrong' do
+ post :destroy, password: 'wrong password'
+
+ expect_password_failure
+ end
+
+ it 'succeeds if password is confirmed' do
+ post :destroy, password: '12345678'
+
+ expect_success
+ end
+ end
+
+ context 'user does not require password confirmation' do
+ before do
+ stub_application_setting(password_authentication_enabled: false)
+ end
+
+ it 'fails if username confirmation is not provided' do
+ post :destroy
+
+ expect_username_failure
+ end
+
+ it 'fails if username confirmation is wrong' do
+ post :destroy, username: 'wrong username'
+
+ expect_username_failure
+ end
+
+ it 'succeeds if username is confirmed' do
+ post :destroy, username: user.username
+
+ expect_success
+ end
end
end
end
diff --git a/spec/factories/ci/build_trace_section_names.rb b/spec/factories/ci/build_trace_section_names.rb
new file mode 100644
index 00000000000..1c16225f0e5
--- /dev/null
+++ b/spec/factories/ci/build_trace_section_names.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :ci_build_trace_section_name, class: Ci::BuildTraceSectionName do
+ sequence(:name) { |n| "section_#{n}" }
+ project factory: :project
+ end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index e5ea6b41ea3..f994c2df821 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -47,6 +47,7 @@ FactoryGirl.define do
trait :invalid do
config(rspec: nil)
+ failure_reason :config_error
end
trait :blocked do
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index e5abfd67d60..0dd1238d6e2 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -12,7 +12,7 @@ FactoryGirl.define do
deployment.project ||= deployment.environment.project
unless deployment.project.repository_exists?
- allow(deployment.project.repository).to receive(:fetch_ref)
+ allow(deployment.project.repository).to receive(:create_ref)
end
end
end
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
index 8303861bcfe..c9ab87a15aa 100644
--- a/spec/factories/emails.rb
+++ b/spec/factories/emails.rb
@@ -2,5 +2,7 @@ FactoryGirl.define do
factory :email do
user
email { generate(:email_alias) }
+
+ trait(:confirmed) { confirmed_at Time.now }
end
end
diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb
new file mode 100644
index 00000000000..f42d36f3d19
--- /dev/null
+++ b/spec/factories/fork_networks.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :fork_network do
+ association :root_project, factory: :project
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
new file mode 100644
index 00000000000..630e40da888
--- /dev/null
+++ b/spec/factories/gcp/cluster.rb
@@ -0,0 +1,38 @@
+FactoryGirl.define do
+ factory :gcp_cluster, class: Gcp::Cluster do
+ project
+ user
+ enabled true
+ gcp_project_id 'gcp-project-12345'
+ gcp_cluster_name 'test-cluster'
+ gcp_cluster_zone 'us-central1-a'
+ gcp_cluster_size 1
+ gcp_machine_type 'n1-standard-4'
+
+ trait :with_kubernetes_service do
+ after(:create) do |cluster, evaluator|
+ create(:kubernetes_service, project: cluster.project).tap do |service|
+ cluster.update(service: service)
+ end
+ end
+ end
+
+ trait :custom_project_namespace do
+ project_namespace 'sample-app'
+ end
+
+ trait :created_on_gke do
+ status_event :make_created
+ endpoint '111.111.111.111'
+ ca_cert 'xxxxxx'
+ kubernetes_token 'xxxxxx'
+ username 'xxxxxx'
+ password 'xxxxxx'
+ end
+
+ trait :errored do
+ status_event :make_errored
+ status_reason 'general error'
+ end
+ end
+end
diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb
new file mode 100644
index 00000000000..e7966cee77b
--- /dev/null
+++ b/spec/factories/gitaly/commit.rb
@@ -0,0 +1,17 @@
+FactoryGirl.define do
+ sequence(:gitaly_commit_id) { Digest::SHA1.hexdigest(Time.now.to_f.to_s) }
+
+ factory :gitaly_commit, class: Gitaly::GitCommit do
+ skip_create
+
+ id { generate(:gitaly_commit_id) }
+ parent_ids do
+ ids = [generate(:gitaly_commit_id), generate(:gitaly_commit_id)]
+ Google::Protobuf::RepeatedField.new(:string, ids)
+ end
+ subject { "My commit" }
+ body { subject + "\nMy body" }
+ author { build(:gitaly_commit_author) }
+ committer { build(:gitaly_commit_author) }
+ end
+end
diff --git a/spec/factories/gitaly/commit_author.rb b/spec/factories/gitaly/commit_author.rb
new file mode 100644
index 00000000000..341873a2002
--- /dev/null
+++ b/spec/factories/gitaly/commit_author.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :gitaly_commit_author, class: Gitaly::CommitAuthor do
+ skip_create
+
+ name { generate(:name) }
+ email { generate(:email) }
+ date { Google::Protobuf::Timestamp.new(seconds: Time.now.to_i) }
+ end
+end
diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb
new file mode 100644
index 00000000000..66ecb44d84b
--- /dev/null
+++ b/spec/factories/gpg_key_subkeys.rb
@@ -0,0 +1,10 @@
+require_relative '../support/gpg_helpers'
+
+FactoryGirl.define do
+ factory :gpg_key_subkey do
+ gpg_key
+
+ sequence(:keyid) { |n| "keyid-#{n}" }
+ sequence(:fingerprint) { |n| "fingerprint-#{n}" }
+ end
+end
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index 1258dce8940..93218e5763e 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -4,5 +4,9 @@ FactoryGirl.define do
factory :gpg_key do
key GpgHelpers::User1.public_key
user
+
+ factory :gpg_key_with_subkeys do
+ key GpgHelpers::User1.public_key_with_extra_signing_key
+ end
end
end
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index c0beecf0bea..e9798ff6a41 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -5,7 +5,7 @@ FactoryGirl.define do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
project
gpg_key
- gpg_key_primary_keyid { gpg_key.primary_keyid }
+ gpg_key_primary_keyid { gpg_key.keyid }
verification_status :verified
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index cbec716d6ea..2c732aaf4ed 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -22,6 +22,11 @@ FactoryGirl.define do
trait :with_diffs do
end
+ trait :with_image_diffs do
+ source_branch "add_images_and_changes"
+ target_branch "master"
+ end
+
trait :without_diffs do
source_branch "improve/awesome"
target_branch "master"
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 7493b0a8b35..4034e7905ad 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -143,6 +143,16 @@ FactoryGirl.define do
end
end
+ trait :wiki_repo do
+ after(:create) do |project|
+ raise 'Failed to create wiki repository!' unless project.create_wiki
+ end
+ end
+
+ trait :read_only do
+ repository_read_only true
+ end
+
trait :broken_repo do
after(:create) do |project|
raise "Failed to create repository!" unless project.create_repository
diff --git a/spec/factories/user_custom_attributes.rb b/spec/factories/user_custom_attributes.rb
new file mode 100644
index 00000000000..278cf290d4f
--- /dev/null
+++ b/spec/factories/user_custom_attributes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user_custom_attribute do
+ user
+ sequence(:key) { |n| "key#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 2144f6ba635..766cd4b0090 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::AbuseReports", js: true do
+describe "Admin::AbuseReports", :js do
let(:user) { create(:user) }
context 'as an admin' do
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index cbccf370514..9cb351282a0 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -40,7 +40,7 @@ feature 'Admin Broadcast Messages' do
expect(page).not_to have_content 'Migration to new server'
end
- scenario 'Live preview a customized broadcast message', js: true do
+ scenario 'Live preview a customized broadcast message', :js do
fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
page.within('.broadcast-message-preview') do
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index e214ae6b78d..6a97378391b 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
feature 'Admin disables 2FA for a user' do
- scenario 'successfully', js: true do
+ scenario 'successfully', :js do
sign_in(create(:admin))
user = create(:user, :two_factor)
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 3768727d8ae..771fb5253da 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -52,7 +52,7 @@ feature 'Admin Groups' do
expect_selected_visibility(internal)
end
- scenario 'when entered in group path, it auto filled the group name', js: true do
+ scenario 'when entered in group path, it auto filled the group name', :js do
visit admin_groups_path
click_link "New group"
group_path = 'gitlab'
@@ -81,7 +81,7 @@ feature 'Admin Groups' do
expect_selected_visibility(group.visibility_level)
end
- scenario 'edit group path does not change group name', js: true do
+ scenario 'edit group path does not change group name', :js do
group = create(:group, :private)
visit admin_group_edit_path(group)
@@ -93,7 +93,7 @@ feature 'Admin Groups' do
end
end
- describe 'add user into a group', js: true do
+ describe 'add user into a group', :js do
shared_context 'adds user into a group' do
it do
visit admin_group_path(group)
@@ -124,7 +124,7 @@ feature 'Admin Groups' do
group.add_user(:user, Gitlab::Access::OWNER)
end
- it 'adds admin a to a group as developer', js: true do
+ it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group)
page.within '.users-group-form' do
@@ -141,7 +141,7 @@ feature 'Admin Groups' do
end
end
- describe 'admin remove himself from a group', js: true do
+ describe 'admin remove himself from a group', :js do
it 'removes admin from the group' do
group.add_user(current_user, Gitlab::Access::DEVELOPER)
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 37fd3e171eb..09e6965849a 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check", feature: true, broken_storage: true do
+feature "Admin Health Check", :feature, :broken_storage do
include StubENV
before do
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 91f08dbad5d..2e65fcc5231 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -74,7 +74,7 @@ describe 'Admin::Hooks', :js do
end
end
- describe 'Test', js: true do
+ describe 'Test', :js do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index ae9b47299e6..a5834056a1d 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'admin issues labels' do
end
end
- it 'deletes all labels', js: true do
+ it 'deletes all labels', :js do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
remove.click
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index f4f2505d436..94b12105088 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -28,7 +28,7 @@ describe "Admin::Projects" do
expect(page).not_to have_content(archived_project.name)
end
- it 'renders all projects', js: true do
+ it 'renders all projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects'
@@ -37,7 +37,7 @@ describe "Admin::Projects" do
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
- it 'renders only archived projects', js: true do
+ it 'renders only archived projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects only'
@@ -74,7 +74,7 @@ describe "Admin::Projects" do
.to receive(:move_uploads_to_new_namespace).and_return(true)
end
- it 'transfers project to group web', js: true do
+ it 'transfers project to group web', :js do
visit admin_project_path(project)
click_button 'Search for Namespace'
@@ -91,7 +91,7 @@ describe "Admin::Projects" do
project.team << [user, :master]
end
- it 'adds admin a to a project as developer', js: true do
+ it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project)
page.within '.users-project-form' do
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 034682dae27..388d30828a7 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Admin > Users > Impersonation Tokens', js: true do
+describe 'Admin > Users > Impersonation Tokens', :js do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index e2e2b13cf8a..f9f4bd6f5b9 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -288,7 +288,7 @@ describe "Admin::Users" do
end
end
- it 'allows group membership to be revoked', js: true do
+ it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
find('.btn-remove').click
end
@@ -309,7 +309,7 @@ describe "Admin::Users" do
end
end
- describe 'remove users secondary email', js: true do
+ describe 'remove users secondary email', :js do
let!(:secondary_email) do
create :email, email: 'secondary@example.com', user: user
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index c2b7543a690..42f5b5eb8dc 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -32,7 +32,7 @@ feature 'Admin uses repository checks' do
end
end
- scenario 'to clear all repository checks', js: true do
+ scenario 'to clear all repository checks', :js do
visit admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index dff6f96b663..4a7c3e4f1ab 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -31,7 +31,7 @@ describe 'Auto deploy' do
expect(page).to have_link('Set up auto deploy')
end
- it 'includes OpenShift as an available template', js: true do
+ it 'includes OpenShift as an available template', :js do
click_link 'Set up auto deploy'
click_button 'Apply a GitLab CI Yaml template'
@@ -40,7 +40,7 @@ describe 'Auto deploy' do
end
end
- it 'creates a merge request using "auto-deploy" branch', js: true do
+ it 'creates a merge request using "auto-deploy" branch', :js do
click_link 'Set up auto deploy'
click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index e010b5f3444..91c4e5037de 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
include DragTo
let(:group) { create(:group, :nested) }
@@ -13,7 +13,7 @@ describe 'Issue Boards', js: true do
project.team << [user, :master]
project.team << [user2, :master]
- allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
+ page.driver.set_cookie('sidebar_collapsed', 'true')
sign_in(user)
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 61b53aa5d1e..435de3861cf 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards shortcut', js: true do
+describe 'Issue Boards shortcut', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f67372337ec..5ac4d87e90b 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards new issue', js: true do
+describe 'Issue Boards new issue', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c3bf50ef9d1..c4817eb947b 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index af4cc00162a..9accd7bb07c 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'CI Lint', js: true do
+describe 'CI Lint', :js do
before do
sign_in(create(:user))
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index ae39ba4da6b..d5e9de20e59 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Container Registry" do
+describe "Container Registry", :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -41,16 +41,19 @@ describe "Container Registry" do
expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(true)
- click_on 'Remove repository'
+ click_on(class: 'js-remove-repo')
end
scenario 'user removes a specific tag from container repository' do
visit_container_registry
+ find('.js-toggle-repo').trigger('click')
+ wait_for_requests
+
expect_any_instance_of(ContainerRegistry::Tag)
.to receive(:delete).and_return(true)
- click_on 'Remove tag'
+ click_on(class: 'js-delete-registry')
end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 3e6a27eafd8..c6ba1211b9e 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Copy as GFM', js: true do
+describe 'Copy as GFM', :js do
include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
@@ -288,8 +288,6 @@ describe 'Copy as GFM', js: true do
'SanitizationFilter',
<<-GFM.strip_heredoc
- <a name="named-anchor"></a>
-
<sub>sub</sub>
<dl>
@@ -448,7 +446,7 @@ describe 'Copy as GFM', js: true do
def verify(label, *gfms)
aggregate_failures(label) do
gfms.each do |gfm|
- html = gfm_to_html(gfm)
+ html = gfm_to_html(gfm).gsub(/\A&#x000A;|&#x000A;\z/, '')
output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip)
end
@@ -465,42 +463,98 @@ describe 'Copy as GFM', js: true do
let(:project) { create(:project, :repository) }
context 'from a diff' do
- before do
- visit project_commit_path(project, sample_commit.id)
- end
+ shared_examples 'copying code from a diff' do
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
- context 'selecting one word of text' do
- it 'copies as inline code' do
- verify(
- '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
+ '`RuntimeError`',
- '`RuntimeError`'
- )
+ target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
+ )
+ end
end
- end
- context 'selecting one line of text' do
- it 'copies as inline code' do
- verify(
- '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]',
- '`raise RuntimeError, "System commands must be given as an array of strings"`'
- )
+ '`raise RuntimeError, "System commands must be given as an array of strings"`',
+
+ target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
+ )
+ end
+ end
+
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+
+ target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
+ )
+ end
end
end
- context 'selecting multiple lines of text' do
- it 'copies as a code block' do
- verify(
- '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+ context 'inline diff' do
+ before do
+ visit project_commit_path(project, sample_commit.id, view: 'inline')
+ end
- <<-GFM.strip_heredoc,
- ```ruby
- raise RuntimeError, "System commands must be given as an array of strings"
- end
- ```
- GFM
- )
+ it_behaves_like 'copying code from a diff'
+ end
+
+ context 'parallel diff' do
+ before do
+ visit project_commit_path(project, sample_commit.id, view: 'parallel')
+ end
+
+ it_behaves_like 'copying code from a diff'
+
+ context 'selecting code on the left' do
+ it 'copies as a code block' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ unless cmd.is_a?(Array)
+ raise "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+
+ target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].left-side'
+ )
+ end
+ end
+
+ context 'selecting code on the right' do
+ it 'copies as a code block' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+
+ target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].right-side'
+ )
+ end
end
end
end
@@ -589,9 +643,9 @@ describe 'Copy as GFM', js: true do
end
end
- def verify(selector, gfm)
+ def verify(selector, gfm, target: nil)
html = html_for_selector(selector)
- output_gfm = html_to_gfm(html, 'transformCodeSelection')
+ output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
expect(output_gfm.strip).to eq(gfm.strip)
end
end
@@ -607,15 +661,21 @@ describe 'Copy as GFM', js: true do
page.evaluate_script(js)
end
- def html_to_gfm(html, transformer = 'transformGFMSelection')
+ def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc
(function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
- node.innerHTML = html;
+ $(html).each(function() { node.appendChild(this) });
+
+ var targetSelector = #{target.to_json};
+ var target;
+ if (targetSelector) {
+ target = document.querySelector(targetSelector);
+ }
- node = transformer(node);
+ node = transformer(node, target);
if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node);
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index bfe9dac3bd4..177cd50dd72 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Cycle Analytics', js: true do
+feature 'Cycle Analytics', :js do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 08d8cc7922b..8bab501134b 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Active Tab', js: true do
+RSpec.describe 'Dashboard Active Tab', :js do
before do
sign_in(create(:user))
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index b6dce1b8ec4..349f9a47112 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Tooltips on .timeago dates', js: true do
+feature 'Tooltips on .timeago dates', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 60a16830cdc..1213f8c32eb 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -5,7 +5,13 @@ RSpec.describe 'Dashboard Group' do
sign_in(create(:user))
end
- it 'creates new group', js: true do
+ it 'defaults sort dropdown to last created' do
+ visit dashboard_groups_path
+
+ expect(page).to have_button('Last created')
+ end
+
+ it 'creates new group', :js do
visit dashboard_groups_path
find('.btn-new').trigger('click')
new_path = 'Samurai'
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index facb67ae787..8759950e013 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -90,17 +90,17 @@ feature 'Dashboard Issues filtering', :js do
context 'sorting' do
it 'shows sorted issues' do
- sorting_by('Oldest updated')
+ sorting_by('Created date')
visit_issues
- expect(find('.issues-filters')).to have_content('Oldest updated')
+ expect(find('.issues-filters')).to have_content('Created date')
end
it 'keeps sorting issues after visiting Projects Issues page' do
- sorting_by('Oldest updated')
+ sorting_by('Created date')
visit project_issues_path(project)
- expect(find('.issues-filters')).to have_content('Oldest updated')
+ expect(find('.issues-filters')).to have_content('Created date')
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 795335aa106..5610894fd9a 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Dashboard Issues' do
expect(page).not_to have_content(other_issue.title)
end
- it 'shows checkmark when unassigned is selected for assignee', js: true do
+ it 'shows checkmark when unassigned is selected for assignee', :js do
find('.js-assignee-search').click
find('li', text: 'Unassigned').click
find('.js-assignee-search').click
@@ -32,7 +32,7 @@ RSpec.describe 'Dashboard Issues' do
expect(find('li[data-user-id="0"] a.is-active')).to be_visible
end
- it 'shows issues when current user is author', js: true do
+ it 'shows issues when current user is author', :js do
find('#assignee_id', visible: false).set('')
find('.js-author-search', match: :first).click
@@ -70,7 +70,7 @@ RSpec.describe 'Dashboard Issues' do
end
describe 'new issue dropdown' do
- it 'shows projects only with issues feature enabled', js: true do
+ it 'shows projects only with issues feature enabled', :js do
find('.new-project-item-select-button').trigger('click')
page.within('.select2-results') do
@@ -79,7 +79,7 @@ RSpec.describe 'Dashboard Issues' do
end
end
- it 'shows the new issue page', js: true do
+ it 'shows the new issue page', :js do
find('.new-project-item-select-button').trigger('click')
wait_for_requests
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index b1a207682c3..6802974c2ee 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dashboard > label filter', js: true do
+describe 'Dashboard > label filter', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index b4992dd54a1..f01ba442e58 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -3,12 +3,13 @@ require 'spec_helper'
feature 'Dashboard Merge Requests' do
include FilterItemSelectHelper
include SortingHelper
+ include ProjectForksHelper
let(:current_user) { create :user }
let(:project) { create(:project) }
let(:public_project) { create(:project, :public, :repository) }
- let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute }
+ let(:forked_project) { fork_project(public_project, current_user, repository: true) }
before do
project.add_master(current_user)
@@ -23,7 +24,7 @@ feature 'Dashboard Merge Requests' do
visit merge_requests_dashboard_path
end
- it 'shows projects only with merge requests feature enabled', js: true do
+ it 'shows projects only with merge requests feature enabled', :js do
find('.new-project-item-select-button').trigger('click')
page.within('.select2-results') do
@@ -88,7 +89,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows authored merge requests', js: true do
+ it 'shows authored merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select(current_user.to_reference, '.js-author-search')
@@ -100,7 +101,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows all merge requests', js: true do
+ it 'shows all merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select('Any Author', '.js-author-search')
@@ -112,19 +113,19 @@ feature 'Dashboard Merge Requests' do
end
it 'shows sorted merge requests' do
- sorting_by('Oldest updated')
+ sorting_by('Created date')
visit merge_requests_dashboard_path(assignee_id: current_user.id)
- expect(find('.issues-filters')).to have_content('Oldest updated')
+ expect(find('.issues-filters')).to have_content('Created date')
end
it 'keeps sorting merge requests after visiting Projects MR page' do
- sorting_by('Oldest updated')
+ sorting_by('Created date')
visit project_merge_requests_path(project)
- expect(find('.issues-filters')).to have_content('Oldest updated')
+ expect(find('.issues-filters')).to have_content('Created date')
end
end
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 4a004107408..8f96899fb4f 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project member activity', js: true do
+feature 'Project member activity', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0613c158c54..fbf8b5c0db6 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -50,6 +50,25 @@ feature 'Dashboard Projects' do
end
end
+ context 'when on Your projects tab' do
+ it 'shows all projects by default' do
+ visit dashboard_projects_path
+
+ expect(page).to have_content(project.name)
+ end
+
+ it 'shows personal projects on personal projects tab', :js do
+ project3 = create(:project, namespace: user.namespace)
+
+ visit dashboard_projects_path
+
+ click_link 'Personal'
+
+ expect(page).not_to have_content(project.name)
+ expect(page).to have_content(project3.name)
+ end
+ end
+
context 'when on Starred projects tab' do
it 'shows only starred projects' do
user.toggle_star(project2)
@@ -61,7 +80,7 @@ feature 'Dashboard Projects' do
end
end
- describe 'with a pipeline', clean_gitlab_redis_shared_state: true do
+ describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do
@@ -83,12 +102,14 @@ feature 'Dashboard Projects' do
end
end
- context 'last push widget' do
+ context 'last push widget', :use_clean_rails_memory_store_caching do
before do
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, ref: 'feature', action: :created)
+ Users::LastPushEventService.new(user).cache_last_push_event(event)
+
visit dashboard_projects_path
end
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 54d477f7274..ad0f132da8c 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > User filters todos', js: true do
+feature 'Dashboard > User filters todos', :js do
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 30bab7eeaa7..01aca443f4a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -17,7 +17,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has a todo', js: true do
+ context 'User has a todo', :js do
before do
create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -177,7 +177,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has done todos', js: true do
+ context 'User has done todos', :js do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -249,7 +249,7 @@ feature 'Dashboard Todos' do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- describe 'mark all as done', js: true do
+ describe 'mark all as done', :js do
before do
visit dashboard_todos_path
find('.js-todos-mark-all').trigger('click')
@@ -267,7 +267,7 @@ feature 'Dashboard Todos' do
end
end
- describe 'undo mark all as done', js: true do
+ describe 'undo mark all as done', :js do
before do
visit dashboard_todos_path
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 357d86497d9..1dd7547a7fc 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Expand and collapse diffs', js: true do
+feature 'Expand and collapse diffs', :js do
let(:branch) { 'expand-collapse-diffs' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index e1c74a24890..c5ec495a418 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -128,12 +128,6 @@ feature 'Top Plus Menu', :js do
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet')
end
- scenario 'public project has no New Issue Button' do
- visit project_path(public_project)
-
- hasnot_topmenuitem("New issue")
- end
-
scenario 'public project has no New merge request menu item' do
visit project_path(public_project)
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 53b3bb3b65f..3c2186b3598 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -49,7 +49,7 @@ describe "GitLab Flavored Markdown" do
end
end
- describe "for issues", js: true do
+ describe "for issues", :js do
before do
@other_issue = create(:issue,
author: user,
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 37814ba6238..d2d0be35f1c 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group variables', js: true do
+feature 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index 1dd09d4f203..2e06caf98f6 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -11,7 +11,7 @@ feature 'Labels subscription' do
gitlab_sign_in user
end
- scenario 'users can subscribe/unsubscribe to group labels', js: true do
+ scenario 'users can subscribe/unsubscribe to group labels', :js do
visit group_labels_path(group)
expect(page).to have_content('feature')
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 2577d98df6f..7ce6a61d50c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Group merge requests page' do
end
it 'ignores archived merge request count badges in navbar' do
- expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1")
+ expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1")
end
it 'ignores archived merge request count badges in state-filters' do
diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
new file mode 100644
index 00000000000..5ed4f3ad2bc
--- /dev/null
+++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+feature 'Groups > User sees users dropdowns in issuables list' do
+ let(:entity) { create(:group) }
+ let(:user_in_dropdown) { create(:user) }
+ let!(:user_not_in_dropdown) { create(:user) }
+ let!(:project) { create(:project, group: entity) }
+
+ before do
+ entity.add_developer(user_in_dropdown)
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:issue, project: project) }
+ let(:issuables_path) { issues_group_path(entity) }
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:merge_request, source_project: project) }
+ let(:issuables_path) { merge_requests_group_path(entity) }
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4ec2e7e6012..862823d862e 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -85,7 +85,7 @@ feature 'Group' do
end
end
- describe 'create a nested group', :nested_groups, js: true do
+ describe 'create a nested group', :nested_groups, :js do
let(:group) { create(:group, path: 'foo') }
context 'as admin' do
@@ -142,7 +142,7 @@ feature 'Group' do
expect(page).not_to have_content('secret-group')
end
- describe 'group edit', js: true do
+ describe 'group edit', :js do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
let(:new_name) { 'new-name' }
@@ -207,7 +207,7 @@ feature 'Group' do
end
end
- describe 'group page with nested groups', :nested_groups, js: true do
+ describe 'group page with nested groups', :nested_groups, :js do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:path) { group_path(group) }
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index b72b690110f..caee7a67aec 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -26,7 +26,7 @@ describe 'Projects > Issuables > Default sort order' do
MergeRequest.all
end
- context 'in the "merge requests" tab', js: true do
+ context 'in the "merge requests" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "last created"' do
@@ -37,19 +37,19 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / open" tab', js: true do
+ context 'in the "merge requests / open" tab', :js do
let(:issuable_type) { :merge_request }
- it 'is "last created"' do
+ it 'is "created date"' do
visit_merge_requests_with_state(project, 'open')
- expect(selected_sort_order).to eq('last created')
+ expect(selected_sort_order).to eq('created date')
expect(first_merge_request).to include(last_created_issuable.title)
expect(last_merge_request).to include(first_created_issuable.title)
end
end
- context 'in the "merge requests / merged" tab', js: true do
+ context 'in the "merge requests / merged" tab', :js do
let(:issuable_type) { :merged_merge_request }
it 'is "last updated"' do
@@ -61,7 +61,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / closed" tab', js: true do
+ context 'in the "merge requests / closed" tab', :js do
let(:issuable_type) { :closed_merge_request }
it 'is "last updated"' do
@@ -73,13 +73,13 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / all" tab', js: true do
+ context 'in the "merge requests / all" tab', :js do
let(:issuable_type) { :merge_request }
- it 'is "last created"' do
+ it 'is "created date"' do
visit_merge_requests_with_state(project, 'all')
- expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(find('.issues-other-filters')).to have_content('Created date')
expect(first_merge_request).to include(last_created_issuable.title)
expect(last_merge_request).to include(first_created_issuable.title)
end
@@ -102,31 +102,31 @@ describe 'Projects > Issuables > Default sort order' do
Issue.all
end
- context 'in the "issues" tab', js: true do
+ context 'in the "issues" tab', :js do
let(:issuable_type) { :issue }
- it 'is "last created"' do
+ it 'is "created date"' do
visit_issues project
- expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(find('.issues-other-filters')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
end
- context 'in the "issues / open" tab', js: true do
+ context 'in the "issues / open" tab', :js do
let(:issuable_type) { :issue }
- it 'is "last created"' do
+ it 'is "created date"' do
visit_issues_with_state(project, 'open')
- expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(find('.issues-other-filters')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
end
- context 'in the "issues / closed" tab', js: true do
+ context 'in the "issues / closed" tab', :js do
let(:issuable_type) { :closed_issue }
it 'is "last updated"' do
@@ -138,13 +138,13 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / all" tab', js: true do
+ context 'in the "issues / all" tab', :js do
let(:issuable_type) { :issue }
- it 'is "last created"' do
+ it 'is "created date"' do
visit_issues_with_state(project, 'all')
- expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(find('.issues-other-filters')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
@@ -157,26 +157,12 @@ describe 'Projects > Issuables > Default sort order' do
visit_issues(project, sort: 'id_desc')
end
- it 'shows the sort order as last created' do
- expect(find('.issues-other-filters')).to have_content('Last created')
+ it 'shows the sort order as created date' do
+ expect(find('.issues-other-filters')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
end
-
- context 'when the sort in the URL is id_asc' do
- let(:issuable_type) { :issue }
-
- before do
- visit_issues(project, sort: 'id_asc')
- end
-
- it 'shows the sort order as oldest created' do
- expect(find('.issues-other-filters')).to have_content('Oldest created')
- expect(first_issue).to include(first_created_issuable.title)
- expect(last_issue).to include(last_created_issuable.title)
- end
- end
end
def selected_sort_order
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7ea29ff252b
--- /dev/null
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when the discussion is unlocked' do
+ it 'the user can lock the issue' do
+ visit project_issue_path(project, issue)
+
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Lock')
+ end
+
+ expect(find('#notes')).to have_content('locked this issue')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Unlock')
+ end
+
+ expect(find('#notes')).to have_content('unlocked this issue')
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+ end
+
+ context 'when a user is not a team member' do
+ context 'when the discussion is unlocked' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not lock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can not create a comment' do
+ page.within('#notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This issue is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb
index 2bd1c8aab86..c6c2e58ecea 100644
--- a/spec/features/issuables/user_sees_sidebar_spec.rb
+++ b/spec/features/issuables/user_sees_sidebar_spec.rb
@@ -12,7 +12,7 @@ describe 'Issue Sidebar on Mobile' do
sign_in(user)
end
- context 'mobile sidebar on merge requests', js: true do
+ context 'mobile sidebar on merge requests', :js do
before do
visit project_merge_request_path(merge_request.project, merge_request)
end
@@ -20,7 +20,7 @@ describe 'Issue Sidebar on Mobile' do
it_behaves_like "issue sidebar stays collapsed on mobile"
end
- context 'mobile sidebar on issues', js: true do
+ context 'mobile sidebar on issues', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index a29acb30163..850b35c4467 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -24,7 +24,7 @@ describe 'Awards Emoji' do
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page', js: true do
+ it 'does not shows a 500 page', :js do
expect(page).to have_text(issue.title)
end
end
@@ -37,37 +37,37 @@ describe 'Awards Emoji' do
wait_for_requests
end
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
- it 'increments the thumbsup emoji', js: true do
+ it 'increments the thumbsup emoji', :js do
find('[data-name="thumbsup"]').click
wait_for_requests
expect(thumbsup_emoji).to have_text("1")
end
- it 'decrements the thumbsdown emoji', js: true do
+ it 'decrements the thumbsdown emoji', :js do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
- it 'decrements the thumbsup emoji', js: true do
+ it 'decrements the thumbsup emoji', :js do
expect(thumbsup_emoji).to have_text("0")
end
end
- it 'toggles the smiley emoji on a note', js: true do
+ it 'toggles the smiley emoji on a note', :js do
toggle_smiley_emoji(true)
within('.note-body') do
@@ -82,7 +82,7 @@ describe 'Awards Emoji' do
end
context 'execute /award quick action' do
- it 'toggles the emoji award on noteable', js: true do
+ it 'toggles the emoji award on noteable', :js do
execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1")
@@ -95,7 +95,7 @@ describe 'Awards Emoji' do
end
end
- context 'unauthorized user', js: true do
+ context 'unauthorized user', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index e95eb19f7d1..ddb69d414da 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue awards', js: true do
+feature 'Issue awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index b2229b44f99..3223eb20b55 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,7 +9,7 @@ feature 'Issues > Labels bulk assignment' do
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
- context 'as an allowed user', js: true do
+ context 'as an allowed user', :js do
before do
project.team << [user, :master]
@@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do
end
def update_issues
- click_button 'Update all'
+ find('.update-selected-issues').trigger('click')
wait_for_requests
end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 80cc8d22999..822ba48e005 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', js: true do
+feature 'Resolving all open discussions in a merge request from an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index ad5fd0fd97b..f0bed85595c 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -24,7 +24,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
end
end
- context 'resolving the discussion', js: true do
+ context 'resolving the discussion', :js do
before do
click_button 'Resolve discussion'
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 3cec59050ab..5e20fb48768 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown author', js: true do
+describe 'Dropdown author', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 44741bcc92d..3012c77f2b9 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown emoji', js: true do
+describe 'Dropdown emoji', :js do
include FilteredSearchHelpers
let!(:project) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index c46803112a9..cbc4f8d4c50 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dropdown label', js: true do
+describe 'Dropdown label', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 3ea6e1c8863..2974016c6a7 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Filter issues', js: true do
+describe 'Filter issues', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
@@ -405,20 +405,18 @@ describe 'Filter issues', js: true do
end
context 'sorting' do
- it 'sorts by oldest updated' do
- create(:issue,
+ it 'sorts by created date' do
+ new_issue = create(:issue,
title: '3 days ago',
project: project,
author: user,
- created_at: 3.days.ago,
- updated_at: 3.days.ago)
+ created_at: 3.days.ago)
- old_issue = create(:issue,
+ create(:issue,
title: '5 days ago',
project: project,
author: user,
- created_at: 5.days.ago,
- updated_at: 5.days.ago)
+ created_at: 5.days.ago)
input_filtered_search('days ago')
@@ -427,10 +425,10 @@ describe 'Filter issues', js: true do
sort_toggle = find('.filtered-search-wrapper .dropdown-toggle')
sort_toggle.click
- find('.filtered-search-wrapper .dropdown-menu li a', text: 'Oldest updated').click
+ find('.filtered-search-wrapper .dropdown-menu li a', text: 'Created date').click
wait_for_requests
- expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title)
+ expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title)
end
end
end
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 5eeecaeda47..eef7988e2bd 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Recent searches', js: true do
+describe 'Recent searches', :js do
include FilteredSearchHelpers
let(:project_1) { create(:project, :public) }
@@ -104,6 +104,6 @@ describe 'Recent searches', js: true do
set_recent_searches(project_1_local_storage_key, 'fail')
visit project_issues_path(project_1)
- expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
+ expect(find('.flash-alert')).to have_text('An error occurred while parsing recent searches')
end
end
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index d4dd570fb37..88688422dc7 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Search bar', js: true do
+describe 'Search bar', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 4ae54fd6f4e..920f5546eef 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Visual tokens', js: true do
+describe 'Visual tokens', :js do
include FilteredSearchHelpers
include WaitForRequests
@@ -28,7 +28,7 @@ describe 'Visual tokens', js: true do
sign_in(user)
create(:issue, project: project)
- allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
+ page.driver.set_cookie('sidebar_collapsed', 'true')
visit project_issues_path(project)
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 2db6f9a2982..8ce470fc288 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -218,54 +218,15 @@ describe 'New/edit issue', :js do
context 'edit issue' do
before do
- visit edit_project_issue_path(project, issue)
- end
-
- it 'allows user to update issue' do
- expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
- expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
- expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
-
- page.within '.js-user-search' do
- expect(page).to have_content user.name
- end
-
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
- end
-
- click_button 'Labels'
- page.within '.dropdown-menu-labels' do
- click_link label.title
- click_link label2.title
- end
- page.within '.js-label-select' do
- expect(page).to have_content label.title
- end
- expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
- expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
-
- click_button 'Save changes'
-
- page.within '.issuable-sidebar' do
- page.within '.assignee' do
- expect(page).to have_content user.name
- end
-
- page.within '.milestone' do
- expect(page).to have_content milestone.title
- end
-
- page.within '.labels' do
- expect(page).to have_content label.title
- expect(page).to have_content label2.title
- end
+ visit project_issue_path(project, issue)
+ page.within('.content .issuable-actions') do
+ click_on 'Edit'
end
end
it 'description has autocomplete' do
- find('#issue_description').native.send_keys('')
- fill_in 'issue_description', with: '@'
+ find_field('issue-description').native.send_keys('')
+ fill_in 'issue-description', with: '@'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c6cf6265645..15041ff04ea 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'GFM autocomplete', js: true do
+feature 'GFM autocomplete', :js do
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 28b636f9359..c0c396af93f 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -28,8 +28,7 @@ feature 'Issue Detail', :js do
fill_in 'issue-title', with: 'issue title'
click_button 'Save'
- visit profile_account_path
- click_link 'Delete account'
+ Users::DestroyService.new(user).execute(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index af11b474842..bc9c3d825c1 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue Sidebar' do
sign_in(user)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
@@ -82,7 +82,7 @@ feature 'Issue Sidebar' do
visit_issue(project, issue)
end
- context 'sidebar', js: true do
+ context 'sidebar', :js do
it 'changes size when the screen size is smaller' do
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
@@ -101,7 +101,7 @@ feature 'Issue Sidebar' do
end
end
- context 'editing issue labels', js: true do
+ context 'editing issue labels', :js do
before do
page.within('.block.labels') do
find('.edit-link').click
@@ -114,7 +114,7 @@ feature 'Issue Sidebar' do
end
end
- context 'creating a new label', js: true do
+ context 'creating a new label', :js do
before do
page.within('.block.labels') do
click_link 'Create new'
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index 634ea111dc1..6869c2c869d 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue markdown toolbar', js: true do
+feature 'Issue markdown toolbar', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index b2724945da4..6d7b1b1cd8f 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -37,7 +37,7 @@ feature 'issue move to another project' do
visit issue_path(issue)
end
- scenario 'moving issue to another project', js: true do
+ scenario 'moving issue to another project', :js do
find('.js-move-issue').trigger('click')
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
@@ -49,7 +49,7 @@ feature 'issue move to another project' do
expect(page.current_path).to include project_path(new_project)
end
- scenario 'searching project dropdown', js: true do
+ scenario 'searching project dropdown', :js do
new_project_search.team << [user, :reporter]
find('.js-move-issue').trigger('click')
@@ -63,7 +63,7 @@ feature 'issue move to another project' do
end
end
- context 'user does not have permission to move the issue to a project', js: true do
+ context 'user does not have permission to move the issue to a project', :js do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 332ce78b138..d25231d624c 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'New issue', js: true do
+describe 'New issue', :js do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8405f1cd48d..29a2d38ae18 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Manually create a todo item from issue', js: true do
+feature 'Manually create a todo item from issue', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index bcc6e9bab0f..1f57c110c11 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do
end
def click_update_issues_button
- find('.update-selected-issues').click
+ find('.update-selected-issues').trigger('click')
wait_for_requests
end
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 9261acda9dc..9f5e25ff2cb 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues > User uses quick actions', js: true do
+feature 'Issues > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
@@ -159,7 +159,7 @@ feature 'Issues > User uses quick actions', js: true do
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
- context 'when the project is valid', js: true do
+ context 'when the project is valid' do
let(:target_project) { create(:project, :public) }
before do
@@ -180,7 +180,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is valid but the user not authorized', js: true do
+ context 'when the project is valid but the user not authorized' do
let(:project_unauthorized) {create(:project, :public)}
before do
@@ -196,7 +196,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is invalid', js: true do
+ context 'when the project is invalid' do
before do
sign_in(user)
visit project_issue_path(project, issue)
@@ -210,7 +210,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the user issues multiple commands', js: true do
+ context 'when the user issues multiple commands' do
let(:target_project) { create(:project, :public) }
let(:milestone) { create(:milestone, title: '1.0', project: project) }
let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 5c284a1fe5f..25e99774575 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Issues' do
+describe 'Issues', :js do
include DropzoneHelper
include IssueHelpers
include SortingHelper
@@ -24,113 +24,27 @@ describe 'Issues' do
end
before do
- visit edit_project_issue_path(project, issue)
- find('.js-zen-enter').click
+ visit project_issue_path(project, issue)
+ page.within('.content .issuable-actions') do
+ find('.issuable-edit').click
+ end
+ find('.issue-details .content-block .js-zen-enter').click
end
it 'opens new issue popup' do
- expect(page).to have_content("Issue ##{issue.iid}")
- end
- end
-
- describe 'Editing issue assignee' do
- let!(:issue) do
- create(:issue,
- author: user,
- assignees: [user],
- project: project)
- end
-
- it 'allows user to select unassigned', js: true do
- visit edit_project_issue_path(project, issue)
-
- expect(page).to have_content "Assignee #{user.name}"
-
- first('.js-user-search').click
- click_link 'Unassigned'
-
- click_button 'Save changes'
-
- page.within('.assignee') do
- expect(page).to have_content 'No assignee - assign yourself'
- end
-
- expect(issue.reload.assignees).to be_empty
+ expect(page).to have_content(issue.description)
end
end
- describe 'due date', js: true do
- context 'on new form' do
- before do
- visit new_project_issue_path(project)
- end
-
- it 'saves with due date' do
- date = Date.today.at_beginning_of_month
-
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
- find('#issuable-due-date').click
-
- page.within '.pika-single' do
- click_button date.day
- end
-
- expect(find('#issuable-due-date').value).to eq date.to_s
-
- click_button 'Submit issue'
-
- page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
- end
- end
- end
-
- context 'on edit form' do
- let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
-
- before do
- visit edit_project_issue_path(project, issue)
- end
-
- it 'saves with due date' do
- date = Date.today.at_beginning_of_month
-
- expect(find('#issuable-due-date').value).to eq date.to_s
-
- date = date.tomorrow
-
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
- find('#issuable-due-date').click
-
- page.within '.pika-single' do
- click_button date.day
- end
-
- expect(find('#issuable-due-date').value).to eq date.to_s
-
- click_button 'Save changes'
-
- page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
- end
- end
-
- it 'warns about version conflict' do
- issue.update(title: "New title")
-
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
+ describe 'Issue info' do
+ it 'links to current issue in breadcrubs' do
+ issue = create(:issue, project: project)
- click_button 'Save changes'
+ visit project_issue_path(project, issue)
- expect(page).to have_content 'Someone edited the issue the same time you did'
- end
+ expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue))
end
- end
- describe 'Issue info' do
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
create(:award_emoji, awardable: issue)
@@ -190,19 +104,12 @@ describe 'Issues' do
let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') }
it 'sorts by newest' do
- visit project_issues_path(project, sort: sort_value_recently_created)
+ visit project_issues_path(project, sort: sort_value_created_date)
expect(first_issue).to include('foo')
expect(last_issue).to include('baz')
end
- it 'sorts by oldest' do
- visit project_issues_path(project, sort: sort_value_oldest_created)
-
- expect(first_issue).to include('baz')
- expect(last_issue).to include('foo')
- end
-
it 'sorts by most recently updated' do
baz.updated_at = Time.now + 100
baz.save
@@ -211,36 +118,22 @@ describe 'Issues' do
expect(first_issue).to include('baz')
end
- it 'sorts by least recently updated' do
- baz.updated_at = Time.now - 100
- baz.save
- visit project_issues_path(project, sort: sort_value_oldest_updated)
-
- expect(first_issue).to include('baz')
- end
-
describe 'sorting by due date' do
before do
foo.update(due_date: 1.day.from_now)
bar.update(due_date: 6.days.from_now)
end
- it 'sorts by recently due date' do
- visit project_issues_path(project, sort: sort_value_due_date_soon)
+ it 'sorts by due date' do
+ visit project_issues_path(project, sort: sort_value_due_date)
expect(first_issue).to include('foo')
end
- it 'sorts by least recently due date' do
- visit project_issues_path(project, sort: sort_value_due_date_later)
-
- expect(first_issue).to include('bar')
- end
-
- it 'sorts by least recently due date by excluding nil due dates' do
+ it 'sorts by due date by excluding nil due dates' do
bar.update(due_date: nil)
- visit project_issues_path(project, sort: sort_value_due_date_later)
+ visit project_issues_path(project, sort: sort_value_due_date)
expect(first_issue).to include('foo')
end
@@ -339,19 +232,12 @@ describe 'Issues' do
bar.save
end
- it 'sorts by recently due milestone' do
- visit project_issues_path(project, sort: sort_value_milestone_soon)
+ it 'sorts by milestone' do
+ visit project_issues_path(project, sort: sort_value_milestone)
expect(first_issue).to include('foo')
expect(last_issue).to include('baz')
end
-
- it 'sorts by least recently due milestone' do
- visit project_issues_path(project, sort: sort_value_milestone_later)
-
- expect(first_issue).to include('bar')
- expect(last_issue).to include('baz')
- end
end
describe 'combine filter and sort' do
@@ -365,13 +251,11 @@ describe 'Issues' do
end
it 'sorts with a filter applied' do
- visit project_issues_path(project,
- sort: sort_value_oldest_created,
- assignee_id: user2.id)
+ visit project_issues_path(project, sort: sort_value_created_date, assignee_id: user2.id)
- expect(first_issue).to include('bar')
- expect(last_issue).to include('foo')
- expect(page).not_to have_content 'baz'
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('bar')
+ expect(page).not_to have_content('baz')
end
end
end
@@ -386,7 +270,7 @@ describe 'Issues' do
visit namespace_project_issues_path(user.namespace, project1)
end
- it 'changes incoming email address token', js: true do
+ it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
find('.incoming-email-token-reset').trigger('click')
@@ -402,7 +286,7 @@ describe 'Issues' do
end
end
- describe 'update labels from issue#show', js: true do
+ describe 'update labels from issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
let!(:label) { create(:label, project: project) }
@@ -425,7 +309,7 @@ describe 'Issues' do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.assignee') do
@@ -443,7 +327,7 @@ describe 'Issues' do
expect(issue.reload.assignees).to be_empty
end
- it 'allows user to select an assignee', js: true do
+ it 'allows user to select an assignee', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -464,7 +348,7 @@ describe 'Issues' do
end
end
- it 'allows user to unselect themselves', js: true do
+ it 'allows user to unselect themselves', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -493,7 +377,7 @@ describe 'Issues' do
project.team << [[guest], :guest]
end
- it 'shows assignee text', js: true do
+ it 'shows assignee text', :js do
sign_out(:user)
sign_in(guest)
@@ -508,7 +392,7 @@ describe 'Issues' do
let!(:milestone) { create(:milestone, project: project) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -526,7 +410,7 @@ describe 'Issues' do
expect(issue.reload.milestone).to be_nil
end
- it 'allows user to de-select milestone', js: true do
+ it 'allows user to de-select milestone', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -556,7 +440,7 @@ describe 'Issues' do
issue.save
end
- it 'shows milestone text', js: true do
+ it 'shows milestone text', :js do
sign_out(:user)
sign_in(guest)
@@ -589,7 +473,7 @@ describe 'Issues' do
end
end
- context 'dropzone upload file', js: true do
+ context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
end
@@ -660,7 +544,7 @@ describe 'Issues' do
end
describe 'due date' do
- context 'update due on issue#show', js: true do
+ context 'update due on issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
before do
@@ -704,8 +588,8 @@ describe 'Issues' do
end
end
- describe 'title issue#show', js: true do
- it 'updates the title', js: true do
+ describe 'title issue#show', :js do
+ it 'updates the title', :js do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
visit project_issue_path(project, issue)
@@ -719,20 +603,20 @@ describe 'Issues' do
end
end
- describe 'confidential issue#show', js: true do
+ describe 'confidential issue#show', :js do
it 'shows confidential sibebar information as confidential and can be turned off' do
issue = create(:issue, :confidential, project: project)
visit project_issue_path(project, issue)
- expect(page).to have_css('.confidential-issue-warning')
- expect(page).to have_css('.is-confidential')
- expect(page).not_to have_css('.is-not-confidential')
+ expect(page).to have_css('.issuable-note-warning')
+ expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
+ expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
- expect(page).to have_css('.confidential-warning-message')
+ expect(page).to have_css('.sidebar-item-warning-message')
- within('.confidential-warning-message') do
+ within('.sidebar-item-warning-message') do
find('.btn-close').click
end
@@ -740,7 +624,7 @@ describe 'Issues' do
visit project_issue_path(project, issue)
- expect(page).not_to have_css('.is-confidential')
+ expect(page).not_to have_css('.is-active')
end
end
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index c9983f0941f..6dfabcc7225 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -197,7 +197,7 @@ feature 'Login' do
expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -215,7 +215,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
@@ -260,7 +260,7 @@ feature 'Login' do
'before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -279,7 +279,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index 63fa72650ac..d49d145f254 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request issue assignment', js: true do
+feature 'Merge request issue assignment', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
index e886309133d..a24464f2556 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request awards', js: true do
+feature 'Merge request awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index 1f5e7b55fb0..fbbfe7942be 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Check if mergeable with unresolved discussions', js: true do
+feature 'Check if mergeable with unresolved discussions', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 4b1e1b9a8d4..48f370c3ad4 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Cherry-pick Merge Requests', js: true do
+describe 'Cherry-pick Merge Requests', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index 299b4f5708a..4dd4e40f52c 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request closing issues message', js: true do
+feature 'Merge Request closing issues message', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 2d2c674f8fb..b0432ed8fc6 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge request conflict resolution', js: true do
+feature 'Merge request conflict resolution', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb
new file mode 100644
index 00000000000..93c40ff6443
--- /dev/null
+++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'Creating a merge request from a fork', :js do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let!(:source_project) do
+ fork_project(project, user,
+ repository: true,
+ namespace: user.namespace)
+ end
+
+ before do
+ source_project.add_master(user)
+
+ sign_in(user)
+ end
+
+ shared_examples 'create merge request to other project' do
+ it 'has all possible target projects' do
+ visit project_new_merge_request_path(source_project)
+
+ first('.js-target-project').click
+
+ within('.dropdown-target-project .dropdown-content') do
+ expect(page).to have_content(project.full_path)
+ expect(page).to have_content(target_project.full_path)
+ expect(page).to have_content(source_project.full_path)
+ end
+ end
+
+ it 'allows creating the merge request to another target project' do
+ visit project_merge_requests_path(source_project)
+
+ page.within '.content' do
+ click_link 'New merge request'
+ end
+
+ find('.js-source-branch', match: :first).click
+ find('.dropdown-source-branch .dropdown-content a', match: :first).click
+
+ first('.js-target-project').click
+ find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click
+
+ click_button 'Compare branches and continue'
+
+ wait_for_requests
+
+ expect { click_button 'Submit merge request' }
+ .to change { target_project.merge_requests.reload.size }.by(1)
+ end
+
+ it 'updates the branches when selecting a new target project' do
+ target_project_member = target_project.owner
+ CreateBranchService.new(target_project, target_project_member)
+ .execute('a-brand-new-branch-to-test', 'master')
+ visit project_new_merge_request_path(source_project)
+
+ first('.js-target-project').click
+ find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click
+
+ wait_for_requests
+
+ first('.js-target-branch').click
+
+ within('.dropdown-target-branch .dropdown-content') do
+ expect(page).to have_content('a-brand-new-branch-to-test')
+ end
+ end
+ end
+
+ context 'creating to the source of a fork' do
+ let!(:target_project) { project }
+
+ it_behaves_like('create merge request to other project')
+ end
+
+ context 'creating to a sibling of a fork' do
+ let!(:target_project) do
+ other_user = create(:user)
+ fork_project(project, other_user,
+ repository: true,
+ namespace: other_user.namespace)
+ end
+
+ it_behaves_like('create merge request to other project')
+ end
+end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 96e8027a54d..5402d61da54 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Create New Merge Request', js: true do
+feature 'Create New Merge Request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 09541873f71..d03ddfece74 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -1,21 +1,20 @@
require 'spec_helper'
feature 'Merge request created from fork' do
+ include ProjectForksHelper
+
given(:user) { create(:user) }
given(:project) { create(:project, :public, :repository) }
- given(:fork_project) { create(:project, :public, :repository) }
+ given(:forked_project) { fork_project(project, user, repository: true) }
given!(:merge_request) do
- create(:forked_project_link, forked_to_project: fork_project,
- forked_from_project: project)
-
- create(:merge_request_with_diffs, source_project: fork_project,
+ create(:merge_request_with_diffs, source_project: forked_project,
target_project: project,
description: 'Test merge request')
end
background do
- fork_project.team << [user, :master]
+ forked_project.team << [user, :master]
sign_in user
end
@@ -31,11 +30,11 @@ feature 'Merge request created from fork' do
background do
create(:note_on_commit, note: comment,
- project: fork_project,
+ project: forked_project,
commit_id: merge_request.commit_shas.first)
end
- scenario 'user can reply to the comment', js: true do
+ scenario 'user can reply to the comment', :js do
visit_merge_request(merge_request)
expect(page).to have_content(comment)
@@ -55,10 +54,10 @@ feature 'Merge request created from fork' do
context 'source project is deleted' do
background do
MergeRequests::MergeService.new(project, user).execute(merge_request)
- fork_project.destroy!
+ forked_project.destroy!
end
- scenario 'user can access merge request', js: true do
+ scenario 'user can access merge request', :js do
visit_merge_request(merge_request)
expect(page).to have_content 'Test merge request'
@@ -69,7 +68,7 @@ feature 'Merge request created from fork' do
context 'pipeline present in source project' do
given(:pipeline) do
create(:ci_pipeline,
- project: fork_project,
+ project: forked_project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch)
end
@@ -79,7 +78,7 @@ feature 'Merge request created from fork' do
create(:ci_build, pipeline: pipeline, name: 'spinach')
end
- scenario 'user visits a pipelines page', js: true do
+ scenario 'user visits a pipelines page', :js do
visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Pipelines' }
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 874c6e2ff69..7f69e82af4c 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
# This test serves as a regression test for a bug that caused an error
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
-describe 'Deleted source branch', js: true do
+describe 'Deleted source branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index ca536f2800c..9aa0672feae 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff note avatars', js: true do
+feature 'Diff note avatars', :js do
include NoteInteractionHelpers
let(:user) { create(:user) }
@@ -22,7 +22,7 @@ feature 'Diff note avatars', js: true do
project.team << [user, :master]
sign_in user
- allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
+ page.driver.set_cookie('sidebar_collapsed', 'true')
end
context 'discussion tab' do
@@ -84,7 +84,7 @@ feature 'Diff note avatars', js: true do
end
it 'shows note avatar' do
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
@@ -92,7 +92,7 @@ feature 'Diff note avatars', js: true do
end
it 'shows comment on note avatar' do
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
@@ -100,13 +100,13 @@ feature 'Diff note avatars', js: true do
end
it 'toggles comments when clicking avatar' do
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click
end
expect(page).to have_selector('.notes_holder', visible: false)
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click
end
@@ -122,7 +122,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
expect(page).not_to have_selector('img.js-diff-comment-avatar')
end
end
@@ -138,7 +138,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests
end
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
@@ -158,7 +158,7 @@ feature 'Diff note avatars', js: true do
end
end
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
@@ -176,7 +176,7 @@ feature 'Diff note avatars', js: true do
end
it 'shows extra comment count' do
- page.within find("[id='#{position.line_code(project.repository)}']") do
+ page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click
expect(find('.diff-comments-more-count')).to have_content '+1'
@@ -185,4 +185,10 @@ feature 'Diff note avatars', js: true do
end
end
end
+
+ def find_line(line_code)
+ line = find("[id='#{line_code}']")
+ line = line.find(:xpath, 'preceding-sibling::*[1][self::td]') if line.tag_name == 'td'
+ line
+ end
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index fd110e68e84..637e6036384 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff notes resolve', js: true do
+feature 'Diff notes resolve', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index e9068f722d5..80fb7335989 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-feature 'Diffs URL', js: true do
+feature 'Diffs URL', :js do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -42,8 +44,12 @@ feature 'Diffs URL', js: true do
visit "#{diffs_project_merge_request_path(project, merge_request)}#{fragment}"
end
- it 'shows expanded note' do
- expect(page).to have_selector(fragment, visible: true)
+ it 'shows collapsed note' do
+ wait_for_requests
+
+ expect(page).to have_selector('.discussion-notes.collapsed') do |note_container|
+ expect(note_container).to have_selector(fragment, visible: false)
+ end
end
end
end
@@ -64,7 +70,7 @@ feature 'Diffs URL', js: true do
context 'when editing file' do
let(:author_user) { create(:user) }
let(:user) { create(:user) }
- let(:forked_project) { Projects::ForkService.new(project, author_user).execute }
+ let(:forked_project) { fork_project(project, author_user, repository: true) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
diff --git a/spec/features/merge_requests/discussion_lock_spec.rb b/spec/features/merge_requests/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7bbd3b1e69e
--- /dev/null
+++ b/spec/features/merge_requests/discussion_lock_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can create a comment' do
+ page.within('.issuable-discussion #notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('.issuable-discussion #notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can not create a comment' do
+ page.within('.issuable-discussion #notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This merge request is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 7386e78fb13..4538555c168 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -29,7 +29,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
- it 'allows to unselect "Remove source branch"', js: true do
+ it 'allows to unselect "Remove source branch"', :js do
merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
@@ -42,7 +42,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Remove source branch'
end
- it 'should preserve description textarea height', js: true do
+ it 'should preserve description textarea height', :js do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 166c02a7a7f..8b9ff9be943 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -18,7 +18,7 @@ feature 'Merge Request filtering by Milestone' do
sign_in(user)
end
- scenario 'filters by no Milestone', js: true do
+ scenario 'filters by no Milestone', :js do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -32,7 +32,7 @@ feature 'Merge Request filtering by Milestone' do
expect(page).to have_css('.merge-request', count: 1)
end
- context 'filters by upcoming milestone', js: true do
+ context 'filters by upcoming milestone', :js do
it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -67,7 +67,7 @@ feature 'Merge Request filtering by Milestone' do
end
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
@@ -83,7 +83,7 @@ feature 'Merge Request filtering by Milestone' do
milestone.update(name: "rock 'n' roll")
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index b51ae0890e4..aac295ab940 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -36,7 +36,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
it 'updates to current user' do
expect_assignee_visual_tokens()
end
@@ -69,7 +69,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'milestone', js: true do
+ context 'milestone', :js do
it 'updates to current milestone' do
expect_milestone_visual_tokens()
end
@@ -88,7 +88,7 @@ describe 'Filter merge requests' do
end
end
- describe 'for label from mr#index', js: true do
+ describe 'for label from mr#index', :js do
it 'filters by no label' do
input_filtered_search('label:none')
@@ -137,7 +137,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee and label', js: true do
+ context 'assignee and label', :js do
def expect_assignee_label_visual_tokens
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Filter merge requests' do
visit project_merge_requests_path(project)
end
- context 'only text', js: true do
+ context 'only text', :js do
it 'filters merge requests by searched text' do
input_filtered_search('bug')
@@ -199,7 +199,7 @@ describe 'Filter merge requests' do
end
end
- context 'filters and searches', js: true do
+ context 'filters and searches', :js do
it 'filters by text and label' do
input_filtered_search('Bug')
@@ -277,9 +277,9 @@ describe 'Filter merge requests' do
expect_mr_list_count(2)
- click_button 'Last created'
+ click_button 'Created date'
page.within '.dropdown-menu-sort' do
- click_link 'Oldest created'
+ click_link 'Priority'
end
wait_for_requests
@@ -289,7 +289,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by assignee id', js: true do
+ describe 'filter by assignee id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, assignee_id: user.id)
@@ -312,7 +312,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by author id', js: true do
+ describe 'filter by author id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, author_id: user.id)
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index de98b147d04..758fc9b139d 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -1,8 +1,10 @@
require 'rails_helper'
describe 'New/edit merge request', :js do
+ include ProjectForksHelper
+
let!(:project) { create(:project, :public, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
@@ -170,16 +172,16 @@ describe 'New/edit merge request', :js do
context 'forked project' do
before do
- fork_project.team << [user, :master]
+ forked_project.team << [user, :master]
sign_in(user)
end
context 'new merge request' do
before do
visit project_new_merge_request_path(
- fork_project,
+ forked_project,
merge_request: {
- source_project_id: fork_project.id,
+ source_project_id: forked_project.id,
target_project_id: project.id,
source_branch: 'fix',
target_branch: 'master'
@@ -238,7 +240,7 @@ describe 'New/edit merge request', :js do
context 'edit merge request' do
before do
merge_request = create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
source_branch: 'fix',
target_branch: 'master'
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb
new file mode 100644
index 00000000000..3c53b51e330
--- /dev/null
+++ b/spec/features/merge_requests/image_diff_notes.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+feature 'image diff notes', :js do
+ include NoteInteractionHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ project.team << [user, :master]
+ sign_in user
+
+ page.driver.set_cookie('sidebar_collapsed', 'true')
+
+ # Stub helper to return any blob file as image from public app folder.
+ # This is necessary to run this specs since we don't display repo images in capybara.
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png')
+ end
+
+ context 'create commit diff notes' do
+ commit_id = '2f63565e7aac07bcdadb654e253078b727143ec4'
+
+ describe 'create a new diff note' do
+ before do
+ visit project_commit_path(project, commit_id)
+ create_image_diff_note
+ end
+
+ it 'shows indicator badge on image diff' do
+ indicator = find('.js-image-badge')
+
+ expect(indicator).to have_content('1')
+ end
+
+ it 'shows the avatar badge on the new note' do
+ badge = find('.image-diff-avatar-link .badge')
+
+ expect(badge).to have_content('1')
+ end
+
+ it 'allows collapsing/expanding the discussion notes' do
+ find('.js-diff-notes-toggle', :first).click
+
+ expect(page).not_to have_content('image diff test comment')
+
+ find('.js-diff-notes-toggle').click
+
+ expect(page).to have_content('image diff test comment')
+ end
+ end
+
+ describe 'render commit diff notes' do
+ let(:path) { "files/images/6049019_460s.jpg" }
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+
+ let(:note1_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 10,
+ y: 10,
+ position_type: "image",
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ let(:note2_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 20,
+ y: 20,
+ position_type: "image",
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ let!(:note1) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note1_position, note: 'my note 1') }
+ let!(:note2) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note2_position, note: 'my note 2') }
+
+ before do
+ visit project_commit_path(project, commit.id)
+ wait_for_requests
+ end
+
+ it 'render diff indicators within the image diff frame' do
+ expect(page).to have_css('.js-image-badge', count: 2)
+ end
+
+ it 'shows the diff notes' do
+ expect(page).to have_css('.diff-content .note', count: 2)
+ end
+
+ it 'shows the diff notes with correct avatar badge numbers' do
+ expect(page).to have_css('.image-diff-avatar-link', text: 1)
+ expect(page).to have_css('.image-diff-avatar-link', text: 2)
+ end
+ end
+ end
+
+ %w(inline parallel).each do |view|
+ context "#{view} view" do
+ let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) }
+ let(:path) { "files/images/ee_repo_logo.png" }
+
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 1,
+ position_type: "image",
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
+
+ describe 'creating a new diff note' do
+ before do
+ visit diffs_project_merge_request_path(project, merge_request, view: view)
+ create_image_diff_note
+ end
+
+ it 'shows indicator badge on image diff' do
+ indicator = find('.js-image-badge', match: :first)
+
+ expect(indicator).to have_content('1')
+ end
+
+ it 'shows the avatar badge on the new note' do
+ badge = find('.image-diff-avatar-link .badge', match: :first)
+
+ expect(badge).to have_content('1')
+ end
+
+ it 'allows expanding/collapsing the discussion notes' do
+ page.all('.js-diff-notes-toggle')[0].trigger('click')
+ page.all('.js-diff-notes-toggle')[1].trigger('click')
+
+ expect(page).not_to have_content('image diff test comment')
+
+ page.all('.js-diff-notes-toggle')[0].trigger('click')
+ page.all('.js-diff-notes-toggle')[1].trigger('click')
+
+ expect(page).to have_content('image diff test comment')
+ end
+ end
+ end
+ end
+
+ describe 'discussion tab polling', :js do
+ let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) }
+ let(:path) { "files/images/ee_repo_logo.png" }
+
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 50,
+ y: 50,
+ position_type: "image",
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'render diff indicators within the image frame' do
+ diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ wait_for_requests
+
+ expect(page).to have_selector('.image-comment-badge')
+ expect(page).to have_content(diff_note.note)
+ end
+ end
+end
+
+def create_image_diff_note
+ find('.js-add-image-diff-note-button', match: :first).click
+ page.all('.js-add-image-diff-note-button')[0].trigger('click')
+ find('.diff-content .note-textarea').native.send_keys('image diff test comment')
+ click_button 'Comment'
+ wait_for_requests
+end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 08a3bb84aac..82b2b56ef80 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Clicking toggle commit message link', js: true do
+feature 'Clicking toggle commit message link', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 59e67420333..91f207bd339 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Only allow merge requests to be merged if the pipeline succeeds', js: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', :js do
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -10,7 +10,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
project.team << [merge_request.author, :master]
end
- context 'project does not have CI enabled', js: true do
+ context 'project does not have CI enabled', :js do
it 'allows MR to be merged' do
visit_merge_request(merge_request)
@@ -20,7 +20,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
end
end
- context 'when project has CI enabled', js: true do
+ context 'when project has CI enabled', :js do
given!(:pipeline) do
create(:ci_empty_pipeline,
project: project,
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 347ce788b36..a3fcc27cab0 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Pipelines for Merge Requests', js: true do
+feature 'Pipelines for Merge Requests', :js do
describe 'pipeline tab' do
given(:user) { create(:user) }
given(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
index 55a82bdf2b9..25abbb469ab 100644
--- a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
+++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Resolve outdated diff discussions', js: true do
+feature 'Resolve outdated diff discussions', :js do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
index 9bbf2610bcb..bce36e05e57 100644
--- a/spec/features/merge_requests/target_branch_spec.rb
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Target branch', js: true do
+describe 'Target branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
index dd989fd49b2..fa3d988b27a 100644
--- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
+++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Toggle Whitespace Changes', js: true do
+feature 'Toggle Whitespace Changes', :js do
before do
sign_in(create(:admin))
merge_request = create(:merge_request)
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index 4e5ec9fbd2d..cd92ad22267 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'toggler_behavior', js: true do
+feature 'toggler_behavior', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index e6dc284cba7..1a41fd36a4f 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -10,7 +10,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
sign_in(user)
end
- context 'status', js: true do
+ context 'status', :js do
describe 'close merge request' do
before do
visit project_merge_requests_path(project)
@@ -37,7 +37,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'assignee', js: true do
+ context 'assignee', :js do
describe 'set assignee' do
before do
visit project_merge_requests_path(project)
@@ -67,7 +67,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'milestone', js: true do
+ context 'milestone', :js do
let(:milestone) { create(:milestone, project: project) }
describe 'set milestone' do
@@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def click_update_merge_requests_button
- find('.update-selected-issues').click
+ find('.update-selected-issues').trigger('click')
wait_for_requests
end
end
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 20008b4e7f9..416a0f78a45 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -52,21 +52,13 @@ describe 'Projects > Merge requests > User lists merge requests' do
end
it 'sorts by newest' do
- visit_merge_requests(project, sort: sort_value_recently_created)
+ visit_merge_requests(project, sort: sort_value_created_date)
expect(first_merge_request).to include('fix')
expect(last_merge_request).to include('merge-test')
expect(count_merge_requests).to eq(3)
end
- it 'sorts by oldest' do
- visit_merge_requests(project, sort: sort_value_oldest_created)
-
- expect(first_merge_request).to include('merge-test')
- expect(last_merge_request).to include('fix')
- expect(count_merge_requests).to eq(3)
- end
-
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
@@ -74,33 +66,19 @@ describe 'Projects > Merge requests > User lists merge requests' do
expect(count_merge_requests).to eq(3)
end
- it 'sorts by oldest updated' do
- visit_merge_requests(project, sort: sort_value_oldest_updated)
-
- expect(first_merge_request).to include('markdown')
- expect(count_merge_requests).to eq(3)
- end
-
- it 'sorts by milestone due soon' do
- visit_merge_requests(project, sort: sort_value_milestone_soon)
+ it 'sorts by milestone' do
+ visit_merge_requests(project, sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(3)
end
- it 'sorts by milestone due later' do
- visit_merge_requests(project, sort: sort_value_milestone_later)
-
- expect(first_merge_request).to include('markdown')
- expect(count_merge_requests).to eq(3)
- end
-
- it 'filters on one label and sorts by due soon' do
+ it 'filters on one label and sorts by due date' do
label = create(:label, project: project)
create(:label_link, label: label, target: @fix)
visit_merge_requests(project, label_name: [label.name],
- sort: sort_value_due_date_soon)
+ sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@@ -115,9 +93,9 @@ describe 'Projects > Merge requests > User lists merge requests' do
create(:label_link, label: label2, target: @fix)
end
- it 'sorts by due soon' do
+ it 'sorts by due date' do
visit_merge_requests(project, label_name: [label.name, label2.name],
- sort: sort_value_due_date_soon)
+ sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@@ -127,7 +105,7 @@ describe 'Projects > Merge requests > User lists merge requests' do
it 'sorts by due soon' do
visit_merge_requests(project, label_name: [label.name, label2.name],
assignee_id: user.id,
- sort: sort_value_due_date_soon)
+ sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@@ -137,7 +115,7 @@ describe 'Projects > Merge requests > User lists merge requests' do
visit project_merge_requests_path(project,
label_name: [label.name, label2.name],
assignee_id: user.id,
- sort: sort_value_milestone_soon)
+ sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
end
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index 442ce14eb7e..7a773fb2baa 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
feature 'Merge requests > User posts diff notes', :js do
+ include MergeRequestDiffHelpers
+
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
before do
- allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
+ page.driver.set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
@@ -225,6 +227,7 @@ feature 'Merge requests > User posts diff notes', :js do
write_comment_on_line(line_holder, diff_side)
click_button 'Comment'
+
wait_for_requests
assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset)
@@ -244,36 +247,6 @@ feature 'Merge requests > User posts diff notes', :js do
expect(line[:num]).not_to have_css comment_button_class
end
- def get_line_components(line_holder, diff_side = nil)
- if diff_side.nil?
- get_inline_line_components(line_holder)
- else
- get_parallel_line_components(line_holder, diff_side)
- end
- end
-
- def get_inline_line_components(line_holder)
- { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
- end
-
- def get_parallel_line_components(line_holder, diff_side = nil)
- side_index = diff_side == 'left' ? 0 : 1
- # Wait for `.line_content`
- line_holder.find('.line_content', match: :first)
- # Wait for `.diff-line-num`
- line_holder.find('.diff-line-num', match: :first)
- { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
- end
-
- def click_diff_line(line_holder, diff_side = nil)
- line = get_line_components(line_holder, diff_side)
- line[:content].hover
-
- expect(line[:num]).to have_css comment_button_class
-
- line[:num].find(comment_button_class).trigger 'click'
- end
-
def write_comment_on_line(line_holder, diff_side)
click_diff_line(line_holder, diff_side)
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 95c50df1896..ee0766f1192 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge Requests > User uses quick actions', js: true do
+feature 'Merge Requests > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 8e231fbc281..50f7d721ff3 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request versions', js: true do
+feature 'Merge Request versions', :js do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index c0221525c9f..5658c2c5122 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Widget Deployments Header', js: true do
+feature 'Widget Deployments Header', :js do
describe 'when deployed to an environment' do
given(:user) { create(:user) }
given(:project) { merge_request.target_project }
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index fd991293ee9..2bad3b02250 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -3,10 +3,13 @@ require 'rails_helper'
describe 'Merge request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) }
before do
- project.team << [user, :master]
+ project.add_master(user)
+ project_only_mwps.add_master(user)
sign_in(user)
end
@@ -142,6 +145,38 @@ describe 'Merge request', :js do
end
end
+ context 'view merge request where project has CI setup but no CI status' do
+ before do
+ pipeline = create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ create(:ci_build, pipeline: pipeline)
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'has pipeline error text' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_requests
+
+ expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again')
+ end
+ end
+
+ context 'view merge request in project with only-mwps setting enabled but no CI is setup' do
+ before do
+ visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project)
+ end
+
+ it 'should be allowed to merge' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_requests
+
+ expect(page).to have_selector('.accept-merge-request')
+ expect(find('.accept-merge-request')['disabled']).not_to be(true)
+ end
+ end
+
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
@@ -184,6 +219,28 @@ describe 'Merge request', :js do
end
end
+ context 'view merge request where fast-forward merge is not possible' do
+ before do
+ project.update(merge_requests_ff_only_enabled: true)
+
+ merge_request.update(
+ merge_user: merge_request.author,
+ merge_status: :cannot_be_merged
+ )
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'shows information about the merge error' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_requests
+
+ page.within('.mr-widget-body') do
+ expect(page).to have_content('Fast-forward merge is not possible')
+ end
+ end
+ end
+
context 'merge error' do
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
@@ -199,7 +256,7 @@ describe 'Merge request', :js do
end
end
- context 'user can merge into source project but cannot push to fork', js: true do
+ context 'user can merge into source project but cannot push to fork', :js do
let(:fork_project) { create(:project, :public, :repository) }
let(:user2) { create(:user) }
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
index 624f13922ed..50c5e0bb65f 100644
--- a/spec/features/milestones/show_spec.rb
+++ b/spec/features/milestones/show_spec.rb
@@ -18,9 +18,9 @@ describe 'Milestone show' do
it 'avoids N+1 database queries' do
create(:labeled_issue, issue_params)
- control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count
+ control = ActiveRecord::QueryRecorder.new { visit_milestone }
create_list(:labeled_issue, 10, issue_params)
- expect { visit_milestone }.not_to exceed_query_limit(control_count)
+ expect { visit_milestone }.not_to exceed_query_limit(control)
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index f183dd8cb75..1cddd35fd8a 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -12,11 +12,47 @@ describe 'Profile account page' do
visit profile_account_path
end
- it { expect(page).to have_content('Remove account') }
+ it { expect(page).to have_content('Delete account') }
- it 'deletes the account' do
- expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1)
- expect(current_path).to eq(new_user_session_path)
+ it 'does not immediately delete the account' do
+ click_button 'Delete account'
+
+ expect(User.exists?(user.id)).to be_truthy
+ end
+
+ it 'deletes user', :js do
+ click_button 'Delete account'
+
+ fill_in 'password', with: '12345678'
+
+ page.within '.popup-dialog' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Account scheduled for removal')
+ expect(User.exists?(user.id)).to be_falsy
+ end
+
+ it 'shows invalid password flash message', :js do
+ click_button 'Delete account'
+
+ fill_in 'password', with: 'testing123'
+
+ page.within '.popup-dialog' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Invalid password')
+ end
+
+ it 'does not show delete button when user owns a group' do
+ group = create(:group)
+ group.add_owner(user)
+
+ visit profile_account_path
+
+ expect(page).not_to have_button('Delete account')
+ expect(page).to have_content("Your account is currently an owner in these groups: #{group.name}")
end
end
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
new file mode 100644
index 00000000000..11cc8aae6f3
--- /dev/null
+++ b/spec/features/profiles/emails_spec.rb
@@ -0,0 +1,71 @@
+require 'rails_helper'
+
+feature 'Profile > Emails' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'User adds an email' do
+ before do
+ visit profile_emails_path
+ end
+
+ scenario 'saves the new email' do
+ fill_in('Email', with: 'my@email.com')
+ click_button('Add email address')
+
+ expect(page).to have_content('my@email.com Unverified')
+ expect(page).to have_content("#{user.email} Verified")
+ expect(page).to have_content('Resend confirmation email')
+ end
+
+ scenario 'does not add a duplicate email' do
+ fill_in('Email', with: user.email)
+ click_button('Add email address')
+
+ email = user.emails.find_by(email: user.email)
+ expect(email).to be_nil
+ expect(page).to have_content('Email has already been taken')
+ end
+ end
+
+ scenario 'User removes email' do
+ user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+ expect(page).to have_content("my@email.com")
+
+ click_link('Remove')
+ expect(page).not_to have_content("my@email.com")
+ end
+
+ scenario 'User confirms email' do
+ email = user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+ expect(page).to have_content("#{email.email} Unverified")
+
+ email.confirm
+ expect(email.confirmed?).to be_truthy
+
+ visit profile_emails_path
+ expect(page).to have_content("#{email.email} Verified")
+ end
+
+ scenario 'User re-sends confirmation email' do
+ email = user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+
+ expect { click_link("Resend confirmation email") }.to change { ActionMailer::Base.deliveries.size }
+ expect(page).to have_content("Confirmation email sent to #{email.email}")
+ end
+
+ scenario 'old unconfirmed emails show Send Confirmation button' do
+ email = user.emails.create(email: 'my@email.com')
+ email.update_attribute(:confirmation_sent_at, nil)
+ visit profile_emails_path
+
+ expect(page).not_to have_content('Resend confirmation email')
+ expect(page).to have_content('Send confirmation email')
+ end
+end
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 623e4f341c5..59233e92f93 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -4,7 +4,7 @@ feature 'Profile > GPG Keys' do
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
before do
- login_as(user)
+ sign_in(user)
end
describe 'User adds a key' do
@@ -20,6 +20,18 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('bette.cartwright@example.net Unverified')
expect(page).to have_content(GpgHelpers::User2.fingerprint)
end
+
+ scenario 'with multiple subkeys' do
+ fill_in('Key', with: GpgHelpers::User3.public_key)
+ click_button('Add key')
+
+ expect(page).to have_content('john.doe@example.com Unverified')
+ expect(page).to have_content(GpgHelpers::User3.fingerprint)
+
+ GpgHelpers::User3.subkey_fingerprints.each do |fingerprint|
+ expect(page).to have_content(fingerprint)
+ end
+ end
end
scenario 'User sees their key' do
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index aa71c4dbba4..7d5ba3a7328 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -12,7 +12,7 @@ feature 'Profile > SSH Keys' do
visit profile_keys_path
end
- scenario 'auto-populates the title', js: true do
+ scenario 'auto-populates the title', :js do
fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(page).to have_field("Title", with: "dummy@gitlab.com")
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 45f78444362..8cb240077eb 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -7,7 +7,7 @@ describe 'Profile > Applications' do
sign_in(user)
end
- describe 'User manages applications', js: true do
+ describe 'User manages applications', :js do
it 'deletes an application' do
create(:oauth_application, owner: user)
visit oauth_applications_path
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index f3124bbf29e..a572160dae9 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile > Personal Access Tokens', js: true do
+describe 'Profile > Personal Access Tokens', :js do
let(:user) { create(:user) }
def active_personal_access_tokens
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
deleted file mode 100644
index c935cdfd5c4..00000000000
--- a/spec/features/profiles/preferences_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'spec_helper'
-
-describe 'Profile > Preferences', :js do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- visit profile_preferences_path
- end
-
- describe 'User changes their syntax highlighting theme' do
- it 'creates a flash message' do
- choose 'user_color_scheme_id_5'
-
- wait_for_requests
-
- expect_preferences_saved_message
- end
-
- it 'updates their preference' do
- choose 'user_color_scheme_id_5'
-
- wait_for_requests
- refresh
-
- expect(page).to have_checked_field('user_color_scheme_id_5')
- end
- end
-
- describe 'User changes their default dashboard' do
- it 'creates a flash message' do
- select 'Starred Projects', from: 'user_dashboard'
- click_button 'Save'
-
- wait_for_requests
-
- expect_preferences_saved_message
- end
-
- it 'updates their preference' do
- select 'Starred Projects', from: 'user_dashboard'
- click_button 'Save'
-
- wait_for_requests
-
- find('#logo').click
-
- expect(page).to have_content("You don't have starred projects yet")
- expect(page.current_path).to eq starred_dashboard_projects_path
-
- find('.shortcuts-activity').trigger('click')
-
- expect(page).not_to have_content("You don't have starred projects yet")
- expect(page.current_path).to eq dashboard_projects_path
- end
- end
-
- def expect_preferences_saved_message
- page.within('.flash-container') do
- expect(page).to have_content('Preferences saved.')
- end
- end
-end
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
index 6a4173d43e1..d5fe5bdffc5 100644
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', js: true do
+feature 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/profiles/user_manages_emails_spec.rb b/spec/features/profiles/user_manages_emails_spec.rb
new file mode 100644
index 00000000000..7283c76eb54
--- /dev/null
+++ b/spec/features/profiles/user_manages_emails_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe 'User manages emails' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_emails_path)
+ end
+
+ it "shows user's emails" do
+ expect(page).to have_content(user.email)
+
+ user.emails.each do |email|
+ expect(page).to have_content(email.email)
+ end
+ end
+
+ it 'adds an email' do
+ fill_in('email_email', with: 'my@email.com')
+ click_button('Add')
+
+ email = user.emails.find_by(email: 'my@email.com')
+
+ expect(email).not_to be_nil
+ expect(page).to have_content('my@email.com')
+ expect(page).to have_content(user.email)
+
+ user.emails.each do |email|
+ expect(page).to have_content(email.email)
+ end
+ end
+
+ it 'does not add a duplicate email' do
+ fill_in('email_email', with: user.email)
+ click_button('Add')
+
+ email = user.emails.find_by(email: user.email)
+
+ expect(email).to be_nil
+ expect(page).to have_content(user.email)
+
+ user.emails.each do |email|
+ expect(page).to have_content(email.email)
+ end
+ end
+
+ it 'removes an email' do
+ fill_in('email_email', with: 'my@email.com')
+ click_button('Add')
+
+ email = user.emails.find_by(email: 'my@email.com')
+
+ expect(email).not_to be_nil
+ expect(page).to have_content('my@email.com')
+ expect(page).to have_content(user.email)
+
+ user.emails.each do |email|
+ expect(page).to have_content(email.email)
+ end
+
+ # There should be only one remove button at this time
+ click_link('Remove')
+
+ # Force these to reload as they have been cached
+ user.emails.reload
+ email = user.emails.find_by(email: 'my@email.com')
+
+ expect(email).to be_nil
+ expect(page).not_to have_content('my@email.com')
+ expect(page).to have_content(user.email)
+
+ user.emails.each do |email|
+ expect(page).to have_content(email.email)
+ end
+ end
+end
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 48c1787c8b7..923ca8b1c80 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User visits the notifications tab', js: true do
+feature 'User visits the notifications tab', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
new file mode 100644
index 00000000000..a8c08a680d7
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits the profile account page' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_account_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Account')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
new file mode 100644
index 00000000000..a50ebb29e01
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits the authentication log' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(audit_log_profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Authentication log')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
new file mode 100644
index 00000000000..924ee0e4174
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe 'User visits the profile preferences page' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_preferences_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Preferences')
+ end
+
+ describe 'User changes their syntax highlighting theme', :js do
+ it 'creates a flash message' do
+ choose 'user_color_scheme_id_5'
+
+ wait_for_requests
+
+ expect_preferences_saved_message
+ end
+
+ it 'updates their preference' do
+ choose 'user_color_scheme_id_5'
+
+ wait_for_requests
+ refresh
+
+ expect(page).to have_checked_field('user_color_scheme_id_5')
+ end
+ end
+
+ describe 'User changes their default dashboard', :js do
+ it 'creates a flash message' do
+ select 'Starred Projects', from: 'user_dashboard'
+ click_button 'Save'
+
+ wait_for_requests
+
+ expect_preferences_saved_message
+ end
+
+ it 'updates their preference' do
+ select 'Starred Projects', from: 'user_dashboard'
+ click_button 'Save'
+
+ wait_for_requests
+
+ find('#logo').click
+
+ expect(page).to have_content("You don't have starred projects yet")
+ expect(page.current_path).to eq starred_dashboard_projects_path
+
+ find('.shortcuts-activity').trigger('click')
+
+ expect(page).not_to have_content("You don't have starred projects yet")
+ expect(page.current_path).to eq dashboard_projects_path
+ end
+ end
+
+ def expect_preferences_saved_message
+ page.within('.flash-container') do
+ expect(page).to have_content('Preferences saved.')
+ end
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
new file mode 100644
index 00000000000..6601d3039ed
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits their profile' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Profile')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
new file mode 100644
index 00000000000..685bf44619d
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits the profile SSH keys page' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_keys_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('SSH Keys')
+ end
+end
diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb
index 42b47cb3301..cb69aff8d5f 100644
--- a/spec/features/projects/artifacts/browse_spec.rb
+++ b/spec/features/projects/artifacts/browse_spec.rb
@@ -4,16 +4,15 @@ feature 'Browse artifact', :js do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:browse_url) do
+ browse_path('other_artifacts_0.1.2')
+ end
def browse_path(path)
browse_project_job_artifacts_path(project, job, path)
end
context 'when visiting old URL' do
- let(:browse_url) do
- browse_path('other_artifacts_0.1.2')
- end
-
before do
visit browse_url.sub('/-/jobs', '/builds')
end
@@ -22,4 +21,47 @@ feature 'Browse artifact', :js do
expect(page.current_path).to eq(browse_url)
end
end
+
+ context 'when browsing a directory with an text file' do
+ let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'when the project is public' do
+ it "shows external link icon and styles" do
+ visit browse_url
+
+ link = first('.tree-item-file-external-link')
+
+ expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
+ expect(link[:target]).to eq('_blank')
+ expect(link[:rel]).to include('noopener')
+ expect(link[:rel]).to include('noreferrer')
+ expect(page).to have_selector('.js-artifact-tree-external-icon')
+ end
+ end
+
+ context 'when the project is private' do
+ let!(:private_project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ before do
+ private_project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ it 'shows internal link styles' do
+ visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2')
+
+ expect(page).to have_link('doc_sample.txt')
+ expect(page).not_to have_selector('.js-artifact-tree-external-icon')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
new file mode 100644
index 00000000000..adff0a10f0e
--- /dev/null
+++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe 'User interacts with awards in an issue', :js do
+ let(:issue) { create(:issue, project: project)}
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_issue_path(project, issue))
+ end
+
+ it 'toggles the thumbsup award emoji' do
+ page.within('.awards') do
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.award-control.js-emoji-btn')
+ expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
+
+ page.all('.award-control.js-emoji-btn').each do |element|
+ expect(element['title']).to eq('')
+ end
+
+ page.all('.award-control .js-counter').each do |element|
+ expect(element).to have_content('0')
+ end
+
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+ end
+ end
+
+ it 'toggles a custom award emoji' do
+ page.within('.awards') do
+ page.find('.js-add-award').click
+ end
+
+ page.find('.emoji-menu.is-visible')
+
+ expect(page).to have_selector('.js-emoji-menu-search')
+ expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
+
+ page.within('.emoji-menu-content') do
+ emoji_button = page.first('.js-emoji-btn')
+ emoji_button.hover
+ emoji_button.click
+ end
+
+ page.within('.awards') do
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+
+ expect do
+ page.find('.js-emoji-btn.active').click
+ wait_for_requests
+ end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2)
+ end
+ end
+
+ it 'shows the list of award emoji categories' do
+ page.within('.awards') do
+ page.find('.js-add-award').click
+ end
+
+ page.find('.emoji-menu.is-visible')
+
+ expect(page).to have_selector('.js-emoji-menu-search')
+ expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
+
+ fill_in('emoji-menu-search', with: 'hand')
+
+ page.within('.emoji-menu-content') do
+ expect(page).to have_selector('[data-name="raised_hand"]')
+ end
+ end
+
+ it 'adds an award emoji by a comment' do
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: ':smile:')
+
+ click_button('Comment')
+ end
+
+ expect(page).to have_selector('gl-emoji[data-name="smile"]')
+ end
+end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 89ae891037e..68c4a647958 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -39,7 +39,7 @@ feature 'list of badges' do
end
end
- scenario 'user changes current ref of build status badge', js: true do
+ scenario 'user changes current ref of build status badge', :js do
page.within('.pipeline-status') do
first('.js-project-refs-dropdown').click
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index 1160f674974..c12e56d2c3f 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', js: true do
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 62ac9fd0e95..6c625ed17aa 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Editing file blob', js: true do
+feature 'Editing file blob', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
index 1e3080fa319..9f1fef80ab5 100644
--- a/spec/features/projects/blobs/shortcuts_blob_spec.rb
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -6,7 +6,7 @@ feature 'Blob shortcuts' do
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
let(:sha) { project.repository.commit.sha }
- describe 'On a file(blob)', js: true do
+ describe 'On a file(blob)', :js do
def get_absolute_url(path = "")
"http://#{page.server.host}:#{page.server.port}#{path}"
end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index ad06cee4e81..2f407b13c2f 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do
describe 'when checking branches' do
context 'with artifacts' do
before do
- visit project_branches_path(project)
+ visit project_branches_path(project, search: 'binary-encoding')
end
scenario 'shows download artifacts button' do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index ad4527a0b74..941d34dd660 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -5,12 +5,6 @@ describe 'Branches' do
let(:project) { create(:project, :public, :repository) }
let(:repository) { project.repository }
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
context 'logged in as developer' do
before do
sign_in(user)
@@ -18,12 +12,10 @@ describe 'Branches' do
end
describe 'Initial branches page' do
- it 'shows all the branches' do
+ it 'shows all the branches sorted by last updated by default' do
visit project_branches_path(project)
- repository.branches_sorted_by(:name).first(20).each do |branch|
- expect(page).to have_content("#{branch.name}")
- end
+ expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc))
end
it 'sorts the branches by name' do
@@ -32,22 +24,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown
click_link "Name"
- sorted = repository.branches_sorted_by(:name).first(20).map do |branch|
- Regexp.escape(branch.name)
- end
- expect(page).to have_content(/#{sorted.join(".*")}/)
- end
-
- it 'sorts the branches by last updated' do
- visit project_branches_path(project)
-
- click_button "Last updated" # Open sorting dropdown
- click_link "Last updated"
-
- sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch|
- Regexp.escape(branch.name)
- end
- expect(page).to have_content(/#{sorted.join(".*")}/)
+ expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
end
it 'sorts the branches by oldest updated' do
@@ -56,10 +33,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown
click_link "Oldest updated"
- sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch|
- Regexp.escape(branch.name)
- end
- expect(page).to have_content(/#{sorted.join(".*")}/)
+ expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
end
it 'avoids a N+1 query in branches index' do
@@ -72,7 +46,7 @@ describe 'Branches' do
end
describe 'Find branches' do
- it 'shows filtered branches', js: true do
+ it 'shows filtered branches', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
@@ -84,7 +58,7 @@ describe 'Branches' do
end
describe 'Delete unprotected branch' do
- it 'removes branch after confirmation', js: true do
+ it 'removes branch after confirmation', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
@@ -99,28 +73,6 @@ describe 'Branches' do
expect(find('.all-branches')).to have_selector('li', count: 0)
end
end
-
- describe 'Delete protected branch' do
- before do
- project.add_user(user, :master)
- visit project_protected_branches_path(project)
- set_protected_branch_name('fix')
- click_on "Protect"
-
- within(".protected-branches-list") { expect(page).to have_content('fix') }
- expect(ProtectedBranch.count).to eq(1)
- project.add_user(user, :developer)
- end
-
- it 'does not allow devleoper to removes protected branch', js: true do
- visit project_branches_path(project)
-
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
-
- expect(page).to have_css('.btn-remove.disabled')
- end
- end
end
context 'logged in as master' do
@@ -136,37 +88,6 @@ describe 'Branches' do
expect(page).to have_content("Protected branches can be managed in project settings")
end
end
-
- describe 'Delete protected branch' do
- before do
- visit project_protected_branches_path(project)
- set_protected_branch_name('fix')
- click_on "Protect"
-
- within(".protected-branches-list") { expect(page).to have_content('fix') }
- expect(ProtectedBranch.count).to eq(1)
- end
-
- it 'removes branch after modal confirmation', js: true do
- visit project_branches_path(project)
-
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').trigger(:click)
-
- expect(page).to have_css('.js-delete-branch[disabled]')
- fill_in 'delete_branch_input', with: 'fix'
- click_link 'Delete protected branch'
-
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
-
- expect(page).to have_content('No branches to show')
- end
- end
end
context 'logged out' do
@@ -180,4 +101,13 @@ describe 'Branches' do
end
end
end
+
+ def sorted_branches(repository, count:, sort_by:)
+ sorted_branches =
+ repository.branches_sorted_by(sort_by).first(count).map do |branch|
+ Regexp.escape(branch.name)
+ end
+
+ Regexp.new(sorted_branches.join('.*'))
+ end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
new file mode 100644
index 00000000000..810f2c39b43
--- /dev/null
+++ b/spec/features/projects/clusters_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+feature 'Clusters', :js do
+ let!(:project) { create(:project, :repository) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ end
+
+ context 'when user has signed in Google' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:validate_token).and_return(true)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a new page' do
+ expect(page).to have_button('Create cluster')
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ double.tap do |dbl|
+ allow(dbl).to receive(:status).and_return('RUNNING')
+ allow(dbl).to receive(:self_link)
+ .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(dbl)
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Gcp::Cluster.last.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees an cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the succeccful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user destory the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the succeccful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_button('Create cluster')
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed in Google' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ end
+ end
+end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 740331fe42a..9c57626ea1d 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit pipelines', js: true do
+feature 'project commit pipelines', :js do
given(:project) { create(:project, :repository) }
background do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7086f56bb1b..c11a95732b2 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
end
end
- context "I cherry-pick a commit from a different branch", js: true do
+ context "I cherry-pick a commit from a different branch", :js do
it do
find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
new file mode 100644
index 00000000000..f0fe4e00acc
--- /dev/null
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+feature 'Commit diff', :js do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in user
+ end
+
+ %w(inline parallel).each do |view|
+ context "#{view} view" do
+ before do
+ visit project_commit_path(project, sample_commit.id, view: view)
+ end
+
+ it "adds comment to diff" do
+ diff_line_num = first('.diff-line-num.new')
+
+ diff_line_num.trigger('mouseover')
+ diff_line_num.find('.js-add-diff-note-button').trigger('click')
+
+ page.within(first('.diff-viewer')) do
+ find('.js-note-text').set 'test comment'
+
+ click_button 'Comment'
+
+ expect(page).to have_content('test comment')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
new file mode 100644
index 00000000000..221f1d7757e
--- /dev/null
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'User reverts a commit', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+ end
+
+ context 'without creating a new merge request' do
+ before do
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+ end
+
+ it 'reverts a commit' do
+ expect(page).to have_content('The commit has been successfully reverted.')
+ end
+
+ it 'does not revert a previously reverted commit' do
+ # Visit the comment again once it was reverted.
+ visit project_commit_path(project, sample_commit.id)
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+
+ context 'with creating a new merge request' do
+ it 'reverts a commit' do
+ page.within('#modal-revert-commit') do
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
+ end
+ end
+end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 82d73fe8531..87ffc2a0b90 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe "Compare", js: true do
+describe "Compare", :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index fe8567ce348..36809240f76 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -17,7 +17,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('http')
end
- scenario 'switches to SSH', js: true do
+ scenario 'switches to SSH', :js do
visit_project
select_protocol('SSH')
@@ -37,7 +37,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('ssh')
end
- scenario 'switches to HTTP', js: true do
+ scenario 'switches to HTTP', :js do
visit_project
select_protocol('HTTP')
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index bc102895aaf..c1307ab640f 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -62,13 +62,43 @@ feature 'Diff file viewer', :js do
end
context 'Image file' do
- before do
- visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4')
+ context 'Replaced' do
+ before do
+ visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4')
+ end
+
+ it 'shows a rendered image' do
+ within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do
+ expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]')
+ end
+ end
+
+ it 'shows view replaced and view file links' do
+ expect(page.all('.file-actions a').length).to eq 2
+ expect(page.all('.file-actions a')[0]).to have_content 'View replaced file @'
+ expect(page.all('.file-actions a')[1]).to have_content 'View file @'
+ end
end
- it 'shows a rendered image' do
- within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do
- expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]')
+ context 'Added' do
+ before do
+ visit_commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9')
+ end
+
+ it 'shows view file link' do
+ expect(page.all('.file-actions a').length).to eq 1
+ expect(page.all('.file-actions a')[0]).to have_content 'View file @'
+ end
+ end
+
+ context 'Deleted' do
+ before do
+ visit_commit('7fd7a459706ee87be6f855fd98ce8c552b15529a')
+ end
+
+ it 'shows view file link' do
+ expect(page.all('.file-actions a').length).to eq 1
+ expect(page.all('.file-actions a')[0]).to have_content 'View file @'
end
end
end
@@ -108,6 +138,19 @@ feature 'Diff file viewer', :js do
end
end
+ context 'renamed file' do
+ before do
+ visit_commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f')
+ end
+
+ it 'shows the filename with diff highlight' do
+ within('.file-header-content') do
+ expect(page).to have_css('.idiff.left.right.deletion')
+ expect(page).to have_content('files/js/commit.coffee')
+ end
+ end
+ end
+
context 'binary file that appears to be text in the first 1024 bytes' do
before do
# The file we're visiting is smaller than 10 KB and we want it collapsed
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 17f914c9c17..7a372757523 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Project edit', js: true do
+feature 'Project edit', :js do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 1c59e57c0a4..610f566c0cf 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -10,26 +10,23 @@ feature 'Environments page', :js do
sign_in(user)
end
- given!(:environment) { }
- given!(:deployment) { }
- given!(:action) { }
-
- before do
- visit_environments(project)
- end
-
describe 'page tabs' do
- scenario 'shows "Available" and "Stopped" tab with links' do
+ it 'shows "Available" and "Stopped" tab with links' do
+ visit_environments(project)
+
expect(page).to have_link('Available')
expect(page).to have_link('Stopped')
end
describe 'with one available environment' do
- given(:environment) { create(:environment, project: project, state: :available) }
+ before do
+ create(:environment, project: project, state: :available)
+ end
describe 'in available tab page' do
it 'should show one environment' do
- visit project_environments_path(project, scope: 'available')
+ visit_environments(project, scope: 'available')
+
expect(page).to have_css('.environments-container')
expect(page.all('.environment-name').length).to eq(1)
end
@@ -37,7 +34,8 @@ feature 'Environments page', :js do
describe 'in stopped tab page' do
it 'should show no environments' do
- visit project_environments_path(project, scope: 'stopped')
+ visit_environments(project, scope: 'stopped')
+
expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
@@ -45,11 +43,14 @@ feature 'Environments page', :js do
end
describe 'with one stopped environment' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
+ before do
+ create(:environment, project: project, state: :stopped)
+ end
describe 'in available tab page' do
it 'should show no environments' do
- visit project_environments_path(project, scope: 'available')
+ visit_environments(project, scope: 'available')
+
expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
@@ -57,7 +58,8 @@ feature 'Environments page', :js do
describe 'in stopped tab page' do
it 'should show one environment' do
- visit project_environments_path(project, scope: 'stopped')
+ visit_environments(project, scope: 'stopped')
+
expect(page).to have_css('.environments-container')
expect(page.all('.environment-name').length).to eq(1)
end
@@ -66,86 +68,84 @@ feature 'Environments page', :js do
end
context 'without environments' do
- scenario 'does show no environments' do
- expect(page).to have_content('You don\'t have any environments right now.')
+ before do
+ visit_environments(project)
end
- scenario 'does show 0 as counter for environments in both tabs' do
+ it 'does not show environments and counters are set to zero' do
+ expect(page).to have_content('You don\'t have any environments right now.')
+
expect(page.find('.js-available-environments-count').text).to eq('0')
expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
end
- describe 'when showing the environment' do
- given(:environment) { create(:environment, project: project) }
-
- scenario 'does show environment name' do
- expect(page).to have_link(environment.name)
- end
-
- scenario 'does show number of available and stopped environments' do
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ describe 'environments table' do
+ given!(:environment) do
+ create(:environment, project: project, state: :available)
end
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('No deployments yet')
+ context 'when there are no deployments' do
+ before do
+ visit_environments(project)
end
- context 'for available environment' do
- given(:environment) { create(:environment, project: project, state: :available) }
+ it 'shows environments names and counters' do
+ expect(page).to have_link(environment.name)
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
- context 'for stopped environment' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
+ it 'does not show deployments' do
+ expect(page).to have_content('No deployments yet')
+ end
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
+ it 'does not show stip button when environment is not stoppable' do
+ expect(page).not_to have_selector('.stop-env-link')
end
end
- context 'with deployments' do
+ context 'when there are deployments' do
given(:project) { create(:project, :repository) }
- given(:deployment) do
+ given!(:deployment) do
create(:deployment, environment: environment,
sha: project.commit.id)
end
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
+ it 'shows deployment SHA and internal ID' do
+ visit_environments(project)
- scenario 'does show deployment internal id' do
+ expect(page).to have_link(deployment.short_sha)
expect(page).to have_content(deployment.iid)
end
- context 'with build and manual actions' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
+ context 'when builds and manual actions are present' do
+ given!(:pipeline) { create(:ci_pipeline, project: project) }
+ given!(:build) { create(:ci_build, pipeline: pipeline) }
- given(:action) do
+ given!(:action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
- given(:deployment) do
+ given!(:deployment) do
create(:deployment, environment: environment,
deployable: build,
sha: project.commit.id)
end
- scenario 'does show a play button' do
+ before do
+ visit_environments(project)
+ end
+
+ it 'shows a play button' do
find('.js-dropdown-play-icon-container').click
+
expect(page).to have_content(action.name.humanize)
end
- scenario 'does allow to play manual action', js: true do
+ it 'allows to play a manual action', :js do
expect(action).to be_manual
find('.js-dropdown-play-icon-container').click
@@ -155,19 +155,19 @@ feature 'Environments page', :js do
.not_to change { Ci::Pipeline.count }
end
- scenario 'does show build name and id' do
+ it 'shows build name and id' do
expect(page).to have_link("#{build.name} ##{build.id}")
end
- scenario 'does not show stop button' do
+ it 'shows a stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
- scenario 'does not show external link button' do
+ it 'does not show external link button' do
expect(page).not_to have_css('external-url')
end
- scenario 'does not show terminal button' do
+ it 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -176,7 +176,7 @@ feature 'Environments page', :js do
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- scenario 'does show an external link button' do
+ it 'shows an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
@@ -192,34 +192,34 @@ feature 'Environments page', :js do
on_stop: 'close_app')
end
- scenario 'does show stop button' do
+ it 'shows a stop button' do
expect(page).to have_selector('.stop-env-link')
end
- context 'for reporter' do
+ context 'when user is a reporter' do
let(:role) { :reporter }
- scenario 'does not show stop button' do
+ it 'does not show stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
end
- context 'with terminal' do
+ context 'when kubernetes terminal is available' do
let(:project) { create(:kubernetes_project, :test_repo) }
context 'for project master' do
let(:role) { :master }
- scenario 'it shows the terminal button' do
+ it 'shows the terminal button' do
expect(page).to have_terminal_button
end
end
- context 'for developer' do
+ context 'when user is a developer' do
let(:role) { :developer }
- scenario 'does not show terminal button' do
+ it 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
@@ -228,59 +228,77 @@ feature 'Environments page', :js do
end
end
- scenario 'does have a New environment button' do
+ it 'does have a new environment button' do
+ visit_environments(project)
+
expect(page).to have_link('New environment')
end
- describe 'when creating a new environment' do
+ describe 'creating a new environment' do
before do
visit_environments(project)
end
- context 'when logged as developer' do
- before do
- within(".top-area") do
- click_link 'New environment'
- end
- end
+ context 'user is a developer' do
+ given(:role) { :developer }
- context 'for valid name' do
- before do
- fill_in('Name', with: 'production')
- click_on 'Save'
- end
+ scenario 'developer creates a new environment with a valid name' do
+ within(".top-area") { click_link 'New environment' }
+ fill_in('Name', with: 'production')
+ click_on 'Save'
- scenario 'does create a new pipeline' do
- expect(page).to have_content('production')
- end
+ expect(page).to have_content('production')
end
- context 'for invalid name' do
- before do
- fill_in('Name', with: 'name,with,commas')
- click_on 'Save'
- end
+ scenario 'developer creates a new environmetn with invalid name' do
+ within(".top-area") { click_link 'New environment' }
+ fill_in('Name', with: 'name,with,commas')
+ click_on 'Save'
- scenario 'does show errors' do
- expect(page).to have_content('Name can contain only letters')
- end
+ expect(page).to have_content('Name can contain only letters')
end
end
- context 'when logged as reporter' do
+ context 'user is a reporter' do
given(:role) { :reporter }
- scenario 'does not have a New environment link' do
+ scenario 'reporters tries to create a new environment' do
expect(page).not_to have_link('New environment')
end
end
end
+ describe 'environments folders' do
+ before do
+ create(:environment, project: project,
+ name: 'staging/review-1',
+ state: :available)
+ create(:environment, project: project,
+ name: 'staging/review-2',
+ state: :available)
+ end
+
+ scenario 'users unfurls an environment folder' do
+ visit_environments(project)
+
+ expect(page).not_to have_content 'review-1'
+ expect(page).not_to have_content 'review-2'
+ expect(page).to have_content 'staging 2'
+
+ within('.folder-row') do
+ find('.folder-name', text: 'staging').click
+ end
+
+ expect(page).to have_content 'review-1'
+ expect(page).to have_content 'review-2'
+ end
+ end
+
def have_terminal_button
have_link(nil, href: terminal_project_environment_path(project, environment))
end
- def visit_environments(project)
- visit project_environments_path(project)
+ def visit_environments(project, **opts)
+ visit project_environments_path(project, **opts)
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 57722276d79..e5282b42a4f 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -6,7 +6,7 @@ describe 'Edit Project Settings' do
let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) }
- describe 'project features visibility selectors', js: true do
+ describe 'project features visibility selectors', :js do
before do
project.team << [member, :master]
sign_in(member)
@@ -163,7 +163,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'repository visibility', js: true do
+ describe 'repository visibility', :js do
before do
project.team << [member, :master]
sign_in(member)
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index f62a9edd37e..84197e45dcb 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', js: true do
+feature 'user browses project', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index cebb238dda1..3c3a5326538 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -16,7 +16,7 @@ feature 'User wants to add a Dockerfile file' do
expect(page).to have_css('.dockerfile-selector')
end
- scenario 'user can pick a Dockerfile file from the dropdown', js: true do
+ scenario 'user can pick a Dockerfile file from the dropdown', :js do
find('.js-dockerfile-selector').click
wait_for_requests
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index c7e3f657639..25f7e18ac5c 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User uses soft wrap whilst editing file', js: true do
+feature 'User uses soft wrap whilst editing file', :js do
before do
user = create(:user)
project = create(:project, :repository)
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 7f97fdb8cc9..618725ee781 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Find file keyboard shortcuts', js: true do
+feature 'Find file keyboard shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index e2044c9d5aa..81d68c3d67c 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector')
end
- scenario 'user can pick a .gitignore file from the dropdown', js: true do
+ scenario 'user can pick a .gitignore file from the dropdown', :js do
find('.js-gitignore-selector').click
wait_for_requests
within '.gitignore-selector' do
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index ab242b0b0b5..8e58fa7bd56 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do
expect(page).to have_css('.gitlab-ci-yml-selector')
end
- scenario 'user can pick a template from the dropdown', js: true do
+ scenario 'user can pick a template from the dropdown', :js do
find('.js-gitlab-ci-yml-selector').click
wait_for_requests
within '.gitlab-ci-yml-selector' do
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 95af263bcac..6c5b1086ec1 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner creates a license file', js: true do
+feature 'project owner creates a license file', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project, :repository) }
background do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 7bcab01c739..6c616bf0456 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner sees a link to create a license file in empty project', js: true do
+feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index 48003eeaa87..f95a60e5194 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template type dropdown selector', js: true do
+feature 'Template type dropdown selector', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 9bcd5beabb8..64fe350f3dc 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template Undo Button', js: true do
+feature 'Template Undo Button', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
new file mode 100644
index 00000000000..e10d29e5eea
--- /dev/null
+++ b/spec/features/projects/fork_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe 'Project fork' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ sign_in user
+ end
+
+ it 'allows user to fork project' do
+ visit project_path(project)
+
+ expect(page).not_to have_css('a.disabled', text: 'Fork')
+ end
+
+ it 'disables fork button when user has exceeded project limit' do
+ user.projects_limit = 0
+ user.save!
+
+ visit project_path(project)
+
+ expect(page).to have_css('a.disabled', text: 'Fork')
+ end
+
+ context 'master in group' do
+ before do
+ group = create(:group)
+ group.add_master(user)
+ end
+
+ it 'allows user to fork project to group or to user namespace' do
+ visit project_path(project)
+
+ expect(page).not_to have_css('a.disabled', text: 'Fork')
+
+ click_link 'Fork'
+
+ expect(page).to have_css('.fork-thumbnail', count: 2)
+ expect(page).not_to have_css('.fork-thumbnail.disabled')
+ end
+
+ it 'allows user to fork project to group and not user when exceeded project limit' do
+ user.projects_limit = 0
+ user.save!
+
+ visit project_path(project)
+
+ expect(page).not_to have_css('a.disabled', text: 'Fork')
+
+ click_link 'Fork'
+
+ expect(page).to have_css('.fork-thumbnail', count: 2)
+ expect(page).to have_css('.fork-thumbnail.disabled')
+ end
+ end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index cff3b1f5743..1c988726ae6 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'GFM autocomplete loading', js: true do
+describe 'GFM autocomplete loading', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 62d244ff259..05776c50f9d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we''l have to either include it adding the model that includes it to the +safe_list+
# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
-feature 'Import/Export - project export integration test', js: true do
+feature 'Import/Export - project export integration test', :js do
include Select2Helper
include ExportFileHelper
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index e5c7781a096..c928459f911 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - project import integration test', js: true do
+feature 'Import/Export - project import integration test', :js do
include Select2Helper
let(:user) { create(:user) }
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index 691b0e1e4ca..b6a7c3cdcdb 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - Namespace export file cleanup', js: true do
+feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index d2789d0aa52..62b23121c5a 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
-feature 'issuable templates', js: true do
+feature 'issuable templates', :js do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
+ let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' }
before do
project.team << [user, :master]
@@ -28,14 +31,17 @@ feature 'issuable templates', js: true do
longtemplate_content,
message: 'added issue template',
branch_name: 'master')
- visit edit_project_issue_path project, issue
- fill_in :'issue[title]', with: 'test issue title'
+ visit project_issue_path project, issue
+ page.within('.content .issuable-actions') do
+ click_on 'Edit'
+ end
+ fill_in :'issue-title', with: 'test issue title'
end
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_requests
- assert_template
+ assert_template(page_part: issue_form_location)
save_changes
end
@@ -43,30 +49,19 @@ feature 'issuable templates', js: true do
select_template 'bug'
wait_for_requests
select_option 'No template'
- assert_template('')
+ assert_template(expected_content: '', page_part: issue_form_location)
save_changes('')
end
scenario 'user selects "bug" template, edits description and then selects "reset template"' do
select_template 'bug'
wait_for_requests
- find_field('issue_description').send_keys(description_addition)
- assert_template(template_content + description_addition)
+ find_field('issue-description').send_keys(description_addition)
+ assert_template(expected_content: template_content + description_addition, page_part: issue_form_location)
select_option 'Reset template'
- assert_template
+ assert_template(page_part: issue_form_location)
save_changes
end
-
- it 'updates height of markdown textarea' do
- start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
-
- select_template 'test'
- wait_for_requests
-
- end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
-
- expect(end_height).not_to eq(start_height)
- end
end
context 'user creates an issue using templates, with a prior description' do
@@ -81,15 +76,18 @@ feature 'issuable templates', js: true do
template_content,
message: 'added issue template',
branch_name: 'master')
- visit edit_project_issue_path project, issue
- fill_in :'issue[title]', with: 'test issue title'
- fill_in :'issue[description]', with: prior_description
+ visit project_issue_path project, issue
+ page.within('.content .issuable-actions') do
+ click_on 'Edit'
+ end
+ fill_in :'issue-title', with: 'test issue title'
+ fill_in :'issue-description', with: prior_description
end
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_requests
- assert_template("#{template_content}")
+ assert_template(page_part: issue_form_location)
save_changes
end
end
@@ -120,15 +118,13 @@ feature 'issuable templates', js: true do
context 'user creates a merge request from a forked project using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
- let(:fork_project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) }
+ let(:forked_project) { fork_project(project, fork_user) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) }
background do
sign_out(:user)
project.team << [fork_user, :developer]
- fork_project.team << [fork_user, :master]
- create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
sign_in(fork_user)
@@ -154,8 +150,10 @@ feature 'issuable templates', js: true do
end
end
- def assert_template(expected_content = template_content)
- expect(find('textarea')['value']).to eq(expected_content)
+ def assert_template(expected_content: template_content, page_part: '#content-body')
+ page.within(page_part) do
+ expect(find('textarea')['value']).to eq(expected_content)
+ end
end
def save_changes(expected_content = template_content)
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
new file mode 100644
index 00000000000..21c9acc7ac0
--- /dev/null
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'User browses a job', :js do
+ let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ project.enable_ci
+ build.success
+ build.trace.set('job trace')
+
+ sign_in(user)
+
+ visit(project_job_path(project, build))
+ end
+
+ it 'erases the job log' do
+ expect(page).to have_content("Job ##{build.id}")
+ expect(page).to have_css('#build-trace')
+
+ click_link('Erase')
+
+ expect(build).not_to have_trace
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(page).to have_no_css('.artifacts')
+
+ page.within('.erased') do
+ expect(page).to have_content('Job has been erased')
+ end
+
+ expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all))
+ end
+end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
new file mode 100644
index 00000000000..767777f3bf9
--- /dev/null
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User browses jobs' do
+ let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ project.enable_ci
+ project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
+
+ sign_in(user)
+
+ visit(project_jobs_path(project))
+ end
+
+ it 'shows the coverage' do
+ page.within('td.coverage') do
+ expect(page).to have_content('99.9%')
+ end
+ end
+
+ it 'shows the "CI Lint" button' do
+ page.within('.nav-controls') do
+ ci_lint_tool_link = page.find_link('CI lint')
+
+ expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path)
+ end
+ end
+end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 3b5c6966287..71702db860c 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -164,9 +164,9 @@ feature 'Jobs' do
end
it 'links to issues/new with the title and description filled in' do
- button_title = "Build Failed ##{job.id}"
- job_path = project_job_path(project, job)
- options = { issue: { title: button_title, description: job_path } }
+ button_title = "Job Failed ##{job.id}"
+ job_url = project_job_path(project, job)
+ options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } }
href = new_project_issue_path(project, options)
@@ -299,7 +299,7 @@ feature 'Jobs' do
end
shared_examples 'expected variables behavior' do
- it 'shows variable key and value after click', js: true do
+ it 'shows variable key and value after click', :js do
expect(page).to have_css('.reveal-variables')
expect(page).not_to have_css('.js-build-variable')
expect(page).not_to have_css('.js-build-value')
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 5716d151250..e8c70dec854 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -13,7 +13,7 @@ feature 'Labels subscription' do
sign_in user
end
- scenario 'users can subscribe/unsubscribe to labels', js: true do
+ scenario 'users can subscribe/unsubscribe to labels', :js do
visit project_labels_path(project)
expect(page).to have_content('bug')
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 8f85e972027..d063f5c27b5 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -17,7 +17,7 @@ feature 'Prioritize labels' do
sign_in user
end
- scenario 'user can prioritize a group label', js: true do
+ scenario 'user can prioritize a group label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -34,7 +34,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a group label', js: true do
+ scenario 'user can unprioritize a group label', :js do
create(:label_priority, project: project, label: feature, priority: 1)
visit project_labels_path(project)
@@ -52,7 +52,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can prioritize a project label', js: true do
+ scenario 'user can prioritize a project label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -69,7 +69,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a project label', js: true do
+ scenario 'user can unprioritize a project label', :js do
create(:label_priority, project: project, label: bug, priority: 1)
visit project_labels_path(project)
@@ -88,7 +88,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can sort prioritized labels and persist across reloads', js: true do
+ scenario 'user can sort prioritized labels and persist across reloads', :js do
create(:label_priority, project: project, label: bug, priority: 1)
create(:label_priority, project: project, label: feature, priority: 2)
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c8988aa63a7..6d729f2f85f 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', js: true do
+feature 'Projects > Members > Group requester cannot request access to project', :js do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 9950272af08..b1053982eee 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Groups with access list', js: true do
+feature 'Projects > Members > Groups with access list', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index cd621b6b3ce..5f7b4ee0e77 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Master adds member with expiration date', js: true do
+feature 'Projects > Members > Master adds member with expiration date', :js do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
new file mode 100644
index 00000000000..c35ba2d7016
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe 'User accepts a merge request', :js do
+ let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'with removing the source branch' do
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'accepts a merge request' do
+ check('Remove source branch')
+ click_button('Merge')
+
+ expect(page).to have_content('The changes were merged into')
+ expect(page).not_to have_selector('.js-remove-branch-button')
+
+ # Wait for View Resource requests to complete so they don't blow up if they are
+ # only handled after `DatabaseCleaner` has already run.
+ wait_for_requests
+ end
+ end
+
+ context 'without removing the source branch' do
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'accepts a merge request' do
+ click_button('Merge')
+
+ expect(page).to have_content('The changes were merged into')
+ expect(page).to have_selector('.js-remove-branch-button')
+
+ # Wait for View Resource requests to complete so they don't blow up if they are
+ # only handled after `DatabaseCleaner` has already run
+ wait_for_requests
+ end
+ end
+
+ context 'when a URL has an anchor' do
+ before do
+ visit(merge_request_path(merge_request, anchor: 'note_123'))
+ end
+
+ it 'accepts a merge request' do
+ check('Remove source branch')
+ click_button('Merge')
+
+ expect(page).to have_content('The changes were merged into')
+ expect(page).not_to have_selector('.js-remove-branch-button')
+
+ # Wait for View Resource requests to complete so they don't blow up if they are
+ # only handled after `DatabaseCleaner` has already run
+ wait_for_requests
+ end
+ end
+
+ context 'when modifying the merge commit message' do
+ before do
+ merge_request.mark_as_mergeable
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'accepts a merge request' do
+ click_button('Modify commit message')
+ fill_in('Commit message', with: 'wow such merge')
+
+ click_button('Merge')
+
+ page.within('.status-box') do
+ expect(page).to have_content('Merged')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
new file mode 100644
index 00000000000..b257f447439
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'User closes a merge requests', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'closes a merge request' do
+ click_link('Close merge request', match: :first)
+
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('Closed by')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..0a952cfc2a9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'User comments on a commit', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ include_examples 'comment on merge request file'
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
new file mode 100644
index 00000000000..f34302f25f8
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe 'User comments on a diff', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(diffs_project_merge_request_path(project, merge_request))
+ end
+
+ context 'when viewing comments' do
+ context 'when toggling inline comments' do
+ context 'in a single file' do
+ it 'hides a comment' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.files > div:nth-child(3)') do
+ expect(page).to have_content('Line is wrong')
+
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'in multiple files' do
+ it 'toggles comments' do
+ click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is correct')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ # Hide the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+
+ # At this moment a user should see only one comment.
+ # The other one should be hidden.
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Show the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+ end
+
+ # Now both the comments should be shown.
+ page.within('.files > div:nth-child(3) .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Check the same comments in the side-by-side view.
+ click_link('Side-by-side')
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(3) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when adding comments' do
+ include_examples 'comment on merge request file'
+ end
+
+ context 'when editing comments' do
+ it 'edits a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form') do
+ fill_in('note_note', with: 'Typo, please fix')
+ click_button('Save comment')
+ end
+
+ expect(page).not_to have_button('Save comment', disabled: true)
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'when deleting comments' do
+ it 'deletes a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
+
+ find('.js-note-delete').click
+ end
+
+ page.within('.merge-request-tabs') do
+ find('.notes-tab').trigger('click')
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_css('.notes .discussion')
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('0')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
new file mode 100644
index 00000000000..2eb652147ce
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'User comments on a merge request', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'adds a comment' do
+ page.within('.js-main-target-form') do
+ fill_in(:note_note, with: '# Comment with a header')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note') do
+ expect(page).to have_content('Comment with a header')
+ expect(page).not_to have_css('#comment-with-a-header')
+ end
+ end
+
+ it 'loads new comment' do
+ # Add new comment in background in order to check
+ # if it's going to be loaded automatically for current user.
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong')
+
+ # Trigger a refresh of notes.
+ execute_script("$(document).trigger('visibilitychange');")
+ wait_for_requests
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
new file mode 100644
index 00000000000..f285c6c8783
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User creates a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_new_merge_request_path(project))
+ end
+
+ it 'creates a merge request' do
+ find('.js-source-branch').click
+ click_link('fix')
+
+ find('.js-target-branch').click
+ click_link('feature')
+
+ click_button('Compare branches')
+
+ fill_in('merge_request_title', with: 'Wiki Feature')
+ click_button('Submit merge request')
+
+ page.within('.merge-request') do
+ expect(page).to have_content('Wiki Feature')
+ end
+
+ wait_for_requests
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
new file mode 100644
index 00000000000..f6e3997383f
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User edits a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'changes the target branch' do
+ expect(page).to have_content('Target branch')
+
+ first('.target_branch').click
+ select('merge-test', from: 'merge_request_target_branch', visible: false)
+ click_button('Save changes')
+
+ expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
+ expect(page).to have_content("changed target branch from #{merge_request.target_branch} to merge-test")
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
new file mode 100644
index 00000000000..30a80f8e652
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User manages subscription', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'toggles subscription' do
+ subscribe_button = find('.issuable-subscribe-button span')
+
+ expect(subscribe_button).to have_content('Subscribe')
+
+ click_on('Subscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Unsubscribe')
+
+ click_on('Unsubscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Subscribe')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
new file mode 100644
index 00000000000..ba3c9789da1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'User reopens a merge requests', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let!(:merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'reopens a merge request' do
+ click_link('Reopen merge request', match: :first)
+
+ page.within('.status-box') do
+ expect(page).to have_content('Open')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
new file mode 100644
index 00000000000..a41d683dbbb
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe 'User reverts a merge request', :js do
+ let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+
+ click_button('Merge')
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'reverts a merge request' do
+ find("a[href='#modal-revert-commit']").click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('The merge request has been successfully reverted.')
+
+ wait_for_requests
+ end
+
+ it 'does not revert a merge request that was previously reverted' do
+ find("a[href='#modal-revert-commit']").click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ find("a[href='#modal-revert-commit']").click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('Sorry, we cannot revert this merge request automatically.')
+ end
+
+ it 'reverts a merge request in a new merge request' do
+ find("a[href='#modal-revert-commit']").click
+
+ page.within('#modal-revert-commit') do
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
new file mode 100644
index 00000000000..d8d9f7e2a8c
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe 'User sorts merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merge_request2) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'keeps the sort option' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Last updated')
+ end
+
+ visit(merge_requests_dashboard_path(assignee_id: user.id))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+
+ visit(project_merge_requests_path(project))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+ end
+
+ context 'when merge requests have awards' do
+ before do
+ create_list(:award_emoji, 2, awardable: merge_request)
+ create(:award_emoji, :downvote, awardable: merge_request)
+
+ create(:award_emoji, awardable: merge_request2)
+ create_list(:award_emoji, 2, :downvote, awardable: merge_request2)
+ end
+
+ it 'sorts by popularity' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Popularity')
+ end
+
+ page.within('.mr-list') do
+ page.within('li.merge-request:nth-child(1)') do
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('2 1')
+ end
+
+ page.within('li.merge-request:nth-child(2)') do
+ expect(page).to have_content(merge_request2.title)
+ expect(page).to have_content('1 2')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
new file mode 100644
index 00000000000..6c695bd7aa9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views all merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :all))
+ end
+
+ it 'shows all merge requests' do
+ expect(page).to have_content(merge_request.title).and have_content(closed_merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
new file mode 100644
index 00000000000..853809fe87a
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views closed merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :closed))
+ end
+
+ it 'shows closed merge requests' do
+ expect(page).to have_content(closed_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
new file mode 100644
index 00000000000..295eb02b625
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'User views diffs', :js do
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ wait_for_requests
+ end
+
+ shared_examples 'unfold diffs' do
+ it 'unfolds diffs' do
+ first('.js-unfold').click
+
+ expect(first('.text-file')).to have_content('.bundle')
+ end
+ end
+
+ it 'shows diffs' do
+ expect(page).to have_css('.tab-content #diffs.active')
+ expect(page).to have_css('#parallel-diff-btn', count: 1)
+ expect(page).to have_css('#inline-diff-btn', count: 1)
+ end
+
+ context 'when in the inline view' do
+ include_examples 'unfold diffs'
+ end
+
+ context 'when in the side-by-side view' do
+ before do
+ click_link('Side-by-side')
+
+ wait_for_requests
+ end
+
+ it 'shows diffs in parallel' do
+ expect(page).to have_css('.parallel')
+ end
+
+ include_examples 'unfold diffs'
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
new file mode 100644
index 00000000000..eb012694f1e
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views merged merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merged_merge_request) { create(:merged_merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :merged))
+ end
+
+ it 'shows merged merge requests' do
+ expect(page).to have_content(merged_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
new file mode 100644
index 00000000000..8970cf54457
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe 'User views an open merge request' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, description: '# Description header')
+ end
+
+ context 'when a merge request does not have repository' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'renders both the title and the description' do
+ node = find('.wiki h1 a#user-content-description-header')
+ expect(node[:href]).to end_with('#description-header')
+
+ # Work around a weird Capybara behavior where calling `parent` on a node
+ # returns the whole document, not the node's actual parent element
+ expect(find(:xpath, "#{node.path}/..").text).to eq(merge_request.description[2..-1])
+
+ expect(page).to have_content(merge_request.title).and have_content(merge_request.description)
+ end
+ end
+
+ context 'when a merge request has repository', :js do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when rendering description preview' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'renders empty description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: '')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_content('Nothing to preview.')
+ end
+ end
+
+ it 'renders description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: ':+1: Nice')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_css('gl-emoji')
+ end
+
+ expect(find('.gfm-form')).to have_css('.js-md-preview').and have_link('Write')
+ expect(find('#merge_request_description', visible: false)).not_to be_visible
+ end
+ end
+
+ context 'when the branch is rebased on the target' do
+ let(:merge_request) { create(:merge_request, :rebased, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'does not show diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).not_to have_content(/([0-9]+ commit[s]? behind)/)
+ end
+ end
+ end
+
+ context 'when the branch is diverged on the target' do
+ let(:merge_request) { create(:merge_request, :diverged, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'shows diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).to have_content(/([0-9]+ commits behind)/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
new file mode 100644
index 00000000000..07b8c1ef479
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'User views open merge requests' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context "when the target branch is the project's default branch" do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows open merge requests' do
+ expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
+ end
+
+ it 'does not show target branch name' do
+ expect(page).to have_content(merge_request.title)
+ expect(find('.issuable-info')).not_to have_content(project.default_branch)
+ end
+ end
+
+ context "when the target branch is different from the project's default branch" do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'feature_conflict')
+ end
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows target branch name' do
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+
+ context 'when a merge request has pipelines' do
+ let!(:build) { create :ci_build, pipeline: pipeline }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request)
+ end
+
+ before do
+ project.enable_ci
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows pipeline status' do
+ page.within('.mr-list') do
+ expect(page).to have_link('Pipeline: pending')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
new file mode 100644
index 00000000000..f6a82f80d65
--- /dev/null
+++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe 'User interacts with labels' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') }
+ let(:issue1) { create(:issue, project: project, title: 'Bugfix1', milestone: milestone) }
+ let(:issue2) { create(:issue, project: project, title: 'Bugfix2', milestone: milestone) }
+ let(:label_bug) { create(:label, project: project, title: 'bug') }
+ let(:label_feature) { create(:label, project: project, title: 'feature') }
+ let(:label_enhancement) { create(:label, project: project, title: 'enhancement') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ issue1.labels << [label_bug, label_feature]
+ issue2.labels << [label_bug, label_enhancement]
+
+ visit(project_milestones_path(project))
+ end
+
+ it 'shows the list of labels', :js do
+ click_link('v2.2')
+
+ page.within('.nav-sidebar') do
+ page.find(:xpath, "//a[@href='#tab-labels']").click
+ end
+
+ expect(page).to have_selector('ul.manage-labels-list')
+
+ wait_for_requests
+
+ page.within('#tab-labels') do
+ expect(page).to have_content(label_bug.title)
+ expect(page).to have_content(label_enhancement.title)
+ expect(page).to have_content(label_feature.title)
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f7b40cb1820..c35b0840248 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -162,6 +162,16 @@ describe 'Pipelines', :js do
expect(page).to have_selector(
%Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end
+
+ it 'contains badge that indicates failure reason' do
+ expect(page).to have_content 'error'
+ end
+
+ it 'contains badge with tooltip which contains failure reason' do
+ expect(pipeline.failure_reason?).to eq true
+ expect(page).to have_selector(
+ %Q{span[data-original-title="#{pipeline.present.failure_reason}"]})
+ end
end
context 'with manual actions' do
@@ -443,7 +453,7 @@ describe 'Pipelines', :js do
visit new_project_pipeline_path(project)
end
- context 'for valid commit', js: true do
+ context 'for valid commit', :js do
before do
click_button project.default_branch
@@ -491,7 +501,7 @@ describe 'Pipelines', :js do
end
describe 'find pipelines' do
- it 'shows filtered pipelines', js: true do
+ it 'shows filtered pipelines', :js do
click_button project.default_branch
page.within '.dropdown-menu' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 5d77cd1ccd5..15a5cd9990b 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -10,7 +10,7 @@ describe 'Edit Project Settings' do
sign_in(user)
end
- describe 'Project settings section', js: true do
+ describe 'Project settings section', :js do
it 'shows errors for invalid project name' do
visit edit_project_path(project)
fill_in 'project_name_edit', with: 'foo&bar'
@@ -32,6 +32,32 @@ describe 'Edit Project Settings' do
end
end
+ describe 'Merge request settings section' do
+ it 'shows "Merge commit" strategy' do
+ visit edit_project_path(project)
+
+ page.within '.merge-requests-feature' do
+ expect(page).to have_content 'Merge commit'
+ end
+ end
+
+ it 'shows "Merge commit with semi-linear history " strategy' do
+ visit edit_project_path(project)
+
+ page.within '.merge-requests-feature' do
+ expect(page).to have_content 'Merge commit with semi-linear history'
+ end
+ end
+
+ it 'shows "Fast-forward merge" strategy' do
+ visit edit_project_path(project)
+
+ page.within '.merge-requests-feature' do
+ expect(page).to have_content 'Fast-forward merge'
+ end
+ end
+ end
+
describe 'Rename repository section' do
context 'with invalid characters' do
it 'shows errors for invalid project path/name' do
@@ -99,7 +125,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'Transfer project section', js: true do
+ describe 'Transfer project section', :js do
let!(:project) { create(:project, :repository, namespace: user.namespace, name: 'gitlabhq') }
let!(:group) { create(:group) }
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index f0a23729220..f8695403857 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Ref switcher', js: true do
+feature 'Ref switcher', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb
deleted file mode 100644
index 65e3a487d4b..00000000000
--- a/spec/features/projects/services/jira_service_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-
-feature 'Setup Jira service', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:service) { project.create_jira_service }
-
- let(:url) { 'http://jira.example.com' }
- let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
-
- def fill_form(active = true)
- check 'Active' if active
-
- fill_in 'service_url', with: url
- fill_in 'service_username', with: 'username'
- fill_in 'service_password', with: 'password'
- fill_in 'service_jira_issue_transition_id', with: '25'
- end
-
- before do
- project.team << [user, :master]
- sign_in(user)
-
- visit project_settings_integrations_path(project)
- end
-
- describe 'user sets and activates Jira Service' do
- context 'when Jira connection test succeeds' do
- it 'activates the JIRA service' do
- server_info = { key: 'value' }.to_json
- WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
-
- click_link('JIRA')
- fill_form
- click_button('Test settings and save changes')
- wait_for_requests
-
- expect(page).to have_content('JIRA activated.')
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
- end
-
- context 'when Jira connection test fails' do
- it 'shows errors when some required fields are not filled in' do
- click_link('JIRA')
-
- check 'Active'
- fill_in 'service_password', with: 'password'
- click_button('Test settings and save changes')
-
- page.within('.service-settings') do
- expect(page).to have_content('This field is required.')
- end
- end
-
- it 'activates the JIRA service' do
- WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password))
- .to_raise(JIRA::HTTPError.new(double(message: 'message')))
-
- click_link('JIRA')
- fill_form
- click_button('Test settings and save changes')
- wait_for_requests
-
- expect(find('.flash-container-page')).to have_content 'Test failed. message'
- expect(find('.flash-container-page')).to have_content 'Save anyway'
-
- find('.flash-alert .flash-action').trigger('click')
- wait_for_requests
-
- expect(page).to have_content('JIRA activated.')
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
- end
- end
-
- describe 'user sets Jira Service but keeps it disabled' do
- context 'when Jira connection test succeeds' do
- it 'activates the JIRA service' do
- click_link('JIRA')
- fill_form(false)
- click_button('Save changes')
-
- expect(page).to have_content('JIRA settings saved, but not activated.')
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
- end
- end
-end
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
deleted file mode 100644
index c10ec5e2987..00000000000
--- a/spec/features/projects/services/slack_service_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-feature 'Projects > Slack service > Setup events' do
- let(:user) { create(:user) }
- let(:service) { SlackService.new }
- let(:project) { create(:project, slack_service: service) }
-
- background do
- service.fields
- service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
- project.team << [user, :master]
- sign_in(user)
- end
-
- scenario 'user can filter events by channel' do
- visit edit_project_service_path(project, service)
-
- expect(page.find_field("service_push_channel").value).to have_content '1'
- expect(page.find_field("service_issue_channel").value).to have_content '2'
- expect(page.find_field("service_merge_request_channel").value).to have_content '3'
- expect(page.find_field("service_note_channel").value).to have_content '4'
- expect(page.find_field("service_tag_push_channel").value).to have_content '5'
- expect(page.find_field("service_pipeline_channel").value).to have_content '6'
- expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
- end
-end
diff --git a/spec/features/projects/services/user_activates_asana_spec.rb b/spec/features/projects/services/user_activates_asana_spec.rb
new file mode 100644
index 00000000000..db836d2985c
--- /dev/null
+++ b/spec/features/projects/services/user_activates_asana_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'User activates Asana' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Asana')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Api key', with: 'verySecret')
+ fill_in('Restrict to branch', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Asana activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_assembla_spec.rb b/spec/features/projects/services/user_activates_assembla_spec.rb
new file mode 100644
index 00000000000..f099b332785
--- /dev/null
+++ b/spec/features/projects/services/user_activates_assembla_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe 'User activates Assembla' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Assembla')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Assembla activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
new file mode 100644
index 00000000000..a00c2e0ad99
--- /dev/null
+++ b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe 'User activates Atlassian Bamboo CI' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Atlassian Bamboo CI')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Bamboo url', with: 'http://bamboo.example.com')
+ fill_in('Build key', with: 'KEY')
+ fill_in('Username', with: 'user')
+ fill_in('Password', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Atlassian Bamboo CI activated.')
+
+ # Password field should not be filled in.
+ click_link('Atlassian Bamboo CI')
+
+ expect(find_field('Enter new password').value).to be_nil
+ end
+end
diff --git a/spec/features/projects/services/user_activates_emails_on_push_spec.rb b/spec/features/projects/services/user_activates_emails_on_push_spec.rb
new file mode 100644
index 00000000000..3769875b29c
--- /dev/null
+++ b/spec/features/projects/services/user_activates_emails_on_push_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe 'User activates Emails on push' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Emails on push')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Recipients', with: 'qa@company.name')
+ click_button('Save')
+
+ expect(page).to have_content('Emails on push activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_flowdock_spec.rb b/spec/features/projects/services/user_activates_flowdock_spec.rb
new file mode 100644
index 00000000000..5298d8acaf5
--- /dev/null
+++ b/spec/features/projects/services/user_activates_flowdock_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe 'User activates Flowdock' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Flowdock')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Flowdock activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb
new file mode 100644
index 00000000000..a9bf16642c7
--- /dev/null
+++ b/spec/features/projects/services/user_activates_hipchat_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'User activates HipChat' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('HipChat')
+ end
+
+ context 'with standart settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+
+ context 'with custom settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab_custom')
+ fill_in('Token', with: 'secretCustom')
+ fill_in('Server', with: 'https://chat.example.com')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+end
diff --git a/spec/features/projects/services/user_activates_irker_spec.rb b/spec/features/projects/services/user_activates_irker_spec.rb
new file mode 100644
index 00000000000..435663c818f
--- /dev/null
+++ b/spec/features/projects/services/user_activates_irker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'User activates Irker (IRC gateway)' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Irker (IRC gateway)')
+ end
+
+ it 'activates service' do
+ check('Active')
+ check('Colorize messages')
+ fill_in('Recipients', with: 'irc://chat.freenode.net/#commits')
+ click_button('Save')
+
+ expect(page).to have_content('Irker (IRC gateway) activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
new file mode 100644
index 00000000000..1048803fde8
--- /dev/null
+++ b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'User activates JetBrains TeamCity CI' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('JetBrains TeamCity CI')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Teamcity url', with: 'http://teamcity.example.com')
+ fill_in('Build type', with: 'GitlabTest_Build')
+ fill_in('Username', with: 'user')
+ fill_in('Password', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('JetBrains TeamCity CI activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
new file mode 100644
index 00000000000..0a86292ae6c
--- /dev/null
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'User activates Jira', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:service) { project.create_jira_service }
+
+ let(:url) { 'http://jira.example.com' }
+ let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_url', with: url
+ fill_in 'service_username', with: 'username'
+ fill_in 'service_password', with: 'password'
+ fill_in 'service_jira_issue_transition_id', with: '25'
+ end
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+
+ visit project_settings_integrations_path(project)
+ end
+
+ describe 'user sets and activates Jira Service' do
+ context 'when Jira connection test succeeds' do
+ it 'activates the JIRA service' do
+ server_info = { key: 'value' }.to_json
+ WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
+
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+ end
+
+ context 'when Jira connection test fails' do
+ it 'shows errors when some required fields are not filled in' do
+ click_link('JIRA')
+
+ check 'Active'
+ fill_in 'service_password', with: 'password'
+ click_button('Test settings and save changes')
+
+ page.within('.service-settings') do
+ expect(page).to have_content('This field is required.')
+ end
+ end
+
+ it 'activates the JIRA service' do
+ WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password))
+ .to_raise(JIRA::HTTPError.new(double(message: 'message')))
+
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed. message'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').trigger('click')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+ end
+ end
+
+ describe 'user sets Jira Service but keeps it disabled' do
+ context 'when Jira connection test succeeds' do
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form(false)
+ click_button('Save changes')
+
+ expect(page).to have_content('JIRA settings saved, but not activated.')
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index 95d5e8b14b9..95d5e8b14b9 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
diff --git a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
new file mode 100644
index 00000000000..d5d109ba48b
--- /dev/null
+++ b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe 'User activates PivotalTracker' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('PivotalTracker')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('PivotalTracker activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_pushover_spec.rb b/spec/features/projects/services/user_activates_pushover_spec.rb
new file mode 100644
index 00000000000..9b7e8d62792
--- /dev/null
+++ b/spec/features/projects/services/user_activates_pushover_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe 'User activates Pushover' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Pushover')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Api key', with: 'verySecret')
+ fill_in('User key', with: 'verySecret')
+ fill_in('Device', with: 'myDevice')
+ select('High Priority', from: 'Priority')
+ select('Bike', from: 'Sound')
+ click_button('Save')
+
+ expect(page).to have_content('Pushover activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
new file mode 100644
index 00000000000..fae9ebd1bd6
--- /dev/null
+++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'User activates Slack notifications' do
+ let(:user) { create(:user) }
+ let(:service) { SlackService.new }
+ let(:project) { create(:project, slack_service: service) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when service is not configured yet' do
+ before do
+ visit(project_settings_integrations_path(project))
+
+ click_link('Slack notifications')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685')
+ click_button('Save')
+
+ expect(page).to have_content('Slack notifications activated.')
+ end
+ end
+
+ context 'when service is already configured' do
+ before do
+ service.fields
+ service.update_attributes(
+ push_channel: 1,
+ issue_channel: 2,
+ merge_request_channel: 3,
+ note_channel: 4,
+ tag_push_channel: 5,
+ pipeline_channel: 6,
+ wiki_page_channel: 7)
+
+ visit(edit_project_service_path(project, service))
+ end
+
+ it 'filters events by channel' do
+ expect(page.find_field('service_push_channel').value).to have_content('1')
+ expect(page.find_field('service_issue_channel').value).to have_content('2')
+ expect(page.find_field('service_merge_request_channel').value).to have_content('3')
+ expect(page.find_field('service_note_channel').value).to have_content('4')
+ expect(page.find_field('service_tag_push_channel').value).to have_content('5')
+ expect(page.find_field('service_pipeline_channel').value).to have_content('6')
+ expect(page.find_field('service_wiki_page_channel').value).to have_content('7')
+ end
+ end
+end
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
index a8baf126269..a8baf126269 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
new file mode 100644
index 00000000000..f86591c2633
--- /dev/null
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User views services' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+ end
+
+ it 'shows the list of available services' do
+ expect(page).to have_content('Project services')
+ expect(page).to have_content('Campfire')
+ expect(page).to have_content('HipChat')
+ expect(page).to have_content('Assembla')
+ expect(page).to have_content('Pushover')
+ expect(page).to have_content('Atlassian Bamboo')
+ expect(page).to have_content('JetBrains TeamCity')
+ expect(page).to have_content('Asana')
+ expect(page).to have_content('Irker (IRC gateway)')
+ end
+end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index d932c4e4d9a..cbdb7973ac8 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -76,7 +76,7 @@ feature 'Integration settings' do
expect(page).to have_content(url)
end
- scenario 'test existing webhook', js: true do
+ scenario 'test existing webhook', :js do
WebMock.stub_request(:post, hook.url)
visit integrations_path
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 975d204e75e..de8fbb15b9c 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -22,7 +22,7 @@ feature "Pipelines settings" do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change', js: true do
+ scenario 'be allowed to change', :js do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 15180d4b498..a4fefb0d0e7 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -23,7 +23,7 @@ feature 'Repository settings' do
context 'for master' do
given(:role) { :master }
- context 'Deploy Keys', js: true do
+ context 'Deploy Keys', :js do
let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) }
let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) }
let(:new_ssh_key) { attributes_for(:key)[:key] }
diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb
new file mode 100644
index 00000000000..91e8059865c
--- /dev/null
+++ b/spec/features/projects/settings/user_manages_group_links_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe 'User manages group links' do
+ include Select2Helper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:group_ops) { create(:group, name: 'Ops') }
+ let(:group_market) { create(:group, name: 'Market', path: 'market') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = group_ops.id
+ share_link.save!
+
+ visit(project_group_links_path(project))
+ end
+
+ it 'shows a list of groups' do
+ page.within('.project-members-groups') do
+ expect(page).to have_content('Ops')
+ expect(page).not_to have_content('Market')
+ end
+ end
+
+ it 'shares a project with a group', :js do
+ click_link('Share with group')
+
+ select2(group_market.id, from: '#link_group_id')
+ select('Master', from: 'link_group_access')
+
+ click_button('Share')
+
+ page.within('.project-members-groups') do
+ expect(page).to have_content('Market')
+ end
+ end
+end
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
new file mode 100644
index 00000000000..2709047b8de
--- /dev/null
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe 'User manages project members' do
+ let(:group) { create(:group, name: 'OpenSource') }
+ let(:project) { create(:project) }
+ let(:project2) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user_dmitriy) { create(:user, name: 'Dmitriy') }
+ let(:user_mike) { create(:user, name: 'Mike') }
+
+ before do
+ project.add_master(user)
+ project.add_developer(user_dmitriy)
+ sign_in(user)
+ end
+
+ it 'cancels a team member' do
+ visit(project_project_members_path(project))
+
+ project_member = project.project_members.find_by(user_id: user_dmitriy.id)
+
+ page.within("#project_member_#{project_member.id}") do
+ click_link('Remove user from project')
+ end
+
+ visit(project_project_members_path(project))
+
+ expect(page).not_to have_content(user_dmitriy.name)
+ expect(page).not_to have_content(user_dmitriy.username)
+ end
+
+ it 'imports a team from another project' do
+ project2.add_master(user)
+ project2.add_reporter(user_mike)
+
+ visit(project_project_members_path(project))
+
+ page.within('.users-project-form') do
+ click_link('Import')
+ end
+
+ select(project2.name_with_namespace, from: 'source_project_id')
+ click_button('Import')
+
+ project_member = project.project_members.find_by(user_id: user_mike.id)
+
+ page.within("#project_member_#{project_member.id}") do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
+ end
+ end
+
+ it 'shows all members of project shared group' do
+ group.add_owner(user)
+ group.add_developer(user_dmitriy)
+
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = group.id
+ share_link.save!
+
+ visit(project_project_members_path(project))
+
+ page.within('.project-members-groups') do
+ expect(page).to have_content('OpenSource')
+ expect(first('.group_member')).to have_content('Master')
+ end
+ end
+end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 37ee6255bd1..1c3b84d0114 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Visibility settings', js: true do
+feature 'Visibility settings', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
diff --git a/spec/features/projects/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb
deleted file mode 100644
index bf18c444c3d..00000000000
--- a/spec/features/projects/shortcuts_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-feature 'Project shortcuts' do
- let(:project) { create(:project, name: 'Victorialand') }
- let(:user) { create(:user) }
-
- describe 'On a project', js: true do
- before do
- project.team << [user, :master]
- sign_in user
- visit project_path(project)
- end
-
- describe 'pressing "i"' do
- it 'redirects to new issue page' do
- find('body').native.send_key('i')
- expect(page).to have_content('Victorialand')
- end
- end
- end
-end
diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb
index 1bc6fae9e7f..0b94c9eae5d 100644
--- a/spec/features/projects/show_project_spec.rb
+++ b/spec/features/projects/show_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project show page', feature: true do
+describe 'Project show page', :feature do
context 'when project pending delete' do
let(:project) { create(:project, :empty_repo, pending_delete: true) }
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
new file mode 100644
index 00000000000..1bd2098af6d
--- /dev/null
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User comments on a snippet', :js do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'leaves a comment on a snippet' do
+ page.within('.js-main-target-form') do
+ fill_in('note_note', with: 'Good snippet!')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Good snippet!')
+ end
+end
diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
new file mode 100644
index 00000000000..ca5f7981c33
--- /dev/null
+++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User deletes a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'deletes a snippet' do
+ first(:link, 'Delete').click
+
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
new file mode 100644
index 00000000000..09a390443cf
--- /dev/null
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User updates a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'updates a snippet' do
+ page.within('.detail-page-header') do
+ first(:link, 'Edit').click
+ end
+
+ fill_in('project_snippet_title', with: 'Snippet new title')
+ click_button('Save')
+
+ expect(page).to have_content('Snippet new title')
+ end
+end
diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb
new file mode 100644
index 00000000000..e9992e00ca8
--- /dev/null
+++ b/spec/features/projects/snippets/user_views_snippets_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User views snippets' do
+ let(:project) { create(:project) }
+ let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
+ let!(:snippet) { create(:snippet, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippets_path(project))
+ end
+
+ it 'shows snippets' do
+ expect(page).to have_content(project_snippet.title)
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/user_archives_project_spec.rb b/spec/features/projects/user_archives_project_spec.rb
new file mode 100644
index 00000000000..72063d13c2a
--- /dev/null
+++ b/spec/features/projects/user_archives_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User archives a project' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ context 'when a project is archived' do
+ let(:project) { create(:project, :archived, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'unarchives a project' do
+ expect(page).to have_content('Unarchive project')
+
+ click_link('Unarchive')
+
+ expect(page).not_to have_content('Archived project')
+ end
+ end
+
+ context 'when a project is unarchived' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'archives a project' do
+ expect(page).to have_content('Archive project')
+
+ click_link('Archive')
+
+ expect(page).to have_content('Archived')
+ end
+ end
+end
diff --git a/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb
new file mode 100644
index 00000000000..a17e65cc5b9
--- /dev/null
+++ b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+# This is a regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/37569
+describe 'User browses a tree with a folder containing only a folder' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:user) { project.creator }
+
+ before do
+ # We need to disable the tree.flat_path provided by Gitaly to reproduce the issue
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+
+ project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder')
+ sign_in(user)
+ visit(project_tree_path(project, project.repository.root_ref))
+ end
+
+ it 'shows the nested folder on a single row' do
+ expect(page).to have_content('foo/bar')
+ end
+end
diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb
index b7a0b72db50..f43b11c9485 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -76,7 +76,7 @@ describe 'User browses files' do
expect(page).to have_content('LICENSE')
end
- it 'shows files from a repository with apostroph in its name', js: true do
+ it 'shows files from a repository with apostroph in its name', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -91,7 +91,7 @@ describe 'User browses files' do
expect(page).not_to have_content('Loading commit data...')
end
- it 'shows the code with a leading dot in the directory', js: true do
+ it 'shows the code with a leading dot in the directory', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -117,7 +117,7 @@ describe 'User browses files' do
click_link('.gitignore')
end
- it 'shows a file content', js: true do
+ it 'shows a file content', :js do
wait_for_requests
expect(page).to have_content('*.rbc')
end
@@ -168,7 +168,7 @@ describe 'User browses files' do
visit(tree_path_root_ref)
end
- it 'shows a preview of a file content', js: true do
+ it 'shows a preview of a file content', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb
index 1ba5d83eadf..052cb3188c5 100644
--- a/spec/features/projects/user_creates_directory_spec.rb
+++ b/spec/features/projects/user_creates_directory_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a directory', js: true do
+feature 'User creates a directory', :js do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
@@ -79,7 +79,7 @@ feature 'User creates a directory', js: true do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Create directory')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
end
diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb
index 3d335687510..cbe70a93942 100644
--- a/spec/features/projects/user_creates_files_spec.rb
+++ b/spec/features/projects/user_creates_files_spec.rb
@@ -59,7 +59,7 @@ describe 'User creates files' do
expect(page).to have_selector('.file-editor')
end
- it 'creates and commit a new file', js: true do
+ it 'creates and commit a new file', :js do
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -74,7 +74,7 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file with new lines at the end of file', js: true do
+ it 'creates and commit a new file with new lines at the end of file', :js do
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -89,7 +89,7 @@ describe 'User creates files' do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
end
- it 'creates and commit a new file with a directory name', js: true do
+ it 'creates and commit a new file with a directory name', :js do
fill_in(:file_name, with: 'foo/bar/baz.txt')
expect(page).to have_selector('.file-editor')
@@ -105,7 +105,7 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file specifying a new branch', js: true do
+ it 'creates and commit a new file specifying a new branch', :js do
expect(page).to have_selector('.file-editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -130,7 +130,7 @@ describe 'User creates files' do
visit(project2_tree_path_root_ref)
end
- it 'creates and commit new file in forked project', js: true do
+ it 'creates and commit new file in forked project', :js do
find('.add-to-tree').click
click_link('New file')
@@ -142,7 +142,7 @@ describe 'User creates files' do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 1c3791f63ac..4a152572502 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a project', js: true do
+feature 'User creates a project', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb
index 95cd316be0e..9e4e92ec076 100644
--- a/spec/features/projects/user_deletes_files_spec.rb
+++ b/spec/features/projects/user_deletes_files_spec.rb
@@ -21,7 +21,7 @@ describe 'User deletes files' do
visit(project_tree_path_root_ref)
end
- it 'deletes the file', js: true do
+ it 'deletes the file', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -41,7 +41,7 @@ describe 'User deletes files' do
visit(project2_tree_path_root_ref)
end
- it 'deletes the file in a forked project', js: true do
+ it 'deletes the file in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -59,7 +59,7 @@ describe 'User deletes files' do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Delete file')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index 3129aad8473..e8d83a661d4 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'User edits files' do
+ include ProjectForksHelper
let(:project) { create(:project, :repository, name: 'Shop') }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
@@ -17,11 +18,10 @@ describe 'User edits files' do
visit(project_tree_path_root_ref)
end
- it 'inserts a content of a file', js: true do
+ it 'inserts a content of a file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -35,11 +35,10 @@ describe 'User edits files' do
expect(page).not_to have_link('edit')
end
- it 'commits an edited file', js: true do
+ it 'commits an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -52,11 +51,11 @@ describe 'User edits files' do
expect(page).to have_content('*.rbca')
end
- it 'commits an edited file to a new branch', js: true do
+ it 'commits an edited file to a new branch', :js do
click_link('.gitignore')
find('.js-edit-blob').click
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -67,15 +66,13 @@ describe 'User edits files' do
click_link('Changes')
- wait_for_requests
expect(page).to have_content('*.rbca')
end
- it 'shows the diff of an edited file', js: true do
+ it 'shows the diff of an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
click_link('Preview changes')
@@ -90,7 +87,7 @@ describe 'User edits files' do
visit(project2_tree_path_root_ref)
end
- it 'inserts a content of a file in a forked project', js: true do
+ it 'inserts a content of a file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -104,14 +101,14 @@ describe 'User edits files' do
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
)
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
- it 'commits an edited file in a forked project', js: true do
+ it 'commits an edited file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -120,13 +117,13 @@ describe 'User edits files' do
click_link('Fork')
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
@@ -134,5 +131,34 @@ describe 'User edits files' do
expect(page).to have_content('New commit message')
end
+
+ context 'when the user already had a fork of the project', :js do
+ let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) }
+ before do
+ visit(project2_tree_path_root_ref)
+ end
+
+ it 'links to the forked project for editing' do
+ click_link('.gitignore')
+ find('.js-edit-blob').click
+
+ expect(page).not_to have_link('Fork')
+ expect(page).not_to have_button('Cancel')
+
+ execute_script("ace.edit('editor').setValue('*.rbca')")
+ fill_in(:commit_message, with: 'Another commit', visible: true)
+ click_button('Commit changes')
+
+ fork = user.fork_of(project2)
+
+ expect(current_path).to eq(project_new_merge_request_path(fork))
+
+ wait_for_requests
+
+ expect(page).to have_content('Another commit')
+ expect(page).to have_content("From #{forked_project.full_path}")
+ expect(page).to have_content("into #{project2.full_path}")
+ end
+ end
end
end
diff --git a/spec/features/projects/user_interacts_with_stars_spec.rb b/spec/features/projects/user_interacts_with_stars_spec.rb
index 0ac3f8181fa..d9d2e0ab171 100644
--- a/spec/features/projects/user_interacts_with_stars_spec.rb
+++ b/spec/features/projects/user_interacts_with_stars_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User interacts with project stars' do
let(:project) { create(:project, :public, :repository) }
- context 'when user is signed in', js: true do
+ context 'when user is signed in', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb
index e284fdefd4f..245b6aa285b 100644
--- a/spec/features/projects/user_replaces_files_spec.rb
+++ b/spec/features/projects/user_replaces_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User replaces files' do
visit(project_tree_path_root_ref)
end
- it 'replaces an existed file with a new one', js: true do
+ it 'replaces an existed file with a new one', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -49,7 +49,7 @@ describe 'User replaces files' do
visit(project2_tree_path_root_ref)
end
- it 'replaces an existed file with a new one in a forked project', js: true do
+ it 'replaces an existed file with a new one in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -74,7 +74,7 @@ describe 'User replaces files' do
expect(page).to have_content('Replacement file commit message')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb
index 98871317ca3..ae51901adc6 100644
--- a/spec/features/projects/user_uploads_files_spec.rb
+++ b/spec/features/projects/user_uploads_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User uploads files' do
visit(project_tree_path_root_ref)
end
- it 'uploads and commit a new file', js: true do
+ it 'uploads and commit a new file', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
@@ -39,6 +39,9 @@ describe 'User uploads files' do
expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
+ find("a[data-action='diffs']", text: 'Changes').click
+
+ wait_for_requests
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
@@ -51,7 +54,7 @@ describe 'User uploads files' do
visit(project2_tree_path_root_ref)
end
- it 'uploads and commit a new fileto a forked project', js: true do
+ it 'uploads and commit a new file to a forked project', :js do
find('.add-to-tree').click
click_link('Upload file')
@@ -69,11 +72,13 @@ describe 'User uploads files' do
expect(page).to have_content('New commit message')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
- click_link('Changes')
+ find("a[data-action='diffs']", text: 'Changes').click
+
+ wait_for_requests
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
new file mode 100644
index 00000000000..fb0d8c766fe
--- /dev/null
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe 'User uses shortcuts', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ context 'when navigating to the Overview pages' do
+ it 'redirects to the details page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('p')
+
+ expect(page).to have_active_navigation('Overview')
+ expect(page).to have_active_sub_navigation('Details')
+ end
+
+ it 'redirects to the activity page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('e')
+
+ expect(page).to have_active_navigation('Overview')
+ expect(page).to have_active_sub_navigation('Activity')
+ end
+ end
+
+ context 'when navigating to the Repository pages' do
+ it 'redirects to the repository files page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('f')
+
+ expect(page).to have_active_navigation('Repository')
+ expect(page).to have_active_sub_navigation('Files')
+ end
+
+ it 'redirects to the repository commits page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('c')
+
+ expect(page).to have_active_navigation('Repository')
+ expect(page).to have_active_sub_navigation('Commits')
+ end
+
+ it 'redirects to the repository graph page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('n')
+
+ expect(page).to have_active_navigation('Repository')
+ expect(page).to have_active_sub_navigation('Graph')
+ end
+
+ it 'redirects to the repository charts page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('d')
+
+ expect(page).to have_active_navigation('Repository')
+ expect(page).to have_active_sub_navigation('Charts')
+ end
+ end
+
+ context 'when navigating to the Issues pages' do
+ it 'redirects to the issues list page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('i')
+
+ expect(page).to have_active_navigation('Issues')
+ expect(page).to have_active_sub_navigation('List')
+ end
+
+ it 'redirects to the new issue page' do
+ find('body').native.send_key('i')
+
+ expect(page).to have_content(project.title)
+ end
+ end
+
+ context 'when navigating to the Merge Requests pages' do
+ it 'redirects to the merge requests page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('m')
+
+ expect(page).to have_active_navigation('Merge Requests')
+ end
+ end
+
+ context 'when navigating to the Snippets pages' do
+ it 'redirects to the snippets page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('s')
+
+ expect(page).to have_active_navigation('Snippets')
+ end
+ end
+
+ context 'when navigating to the Wiki pages' do
+ it 'redirects to the wiki page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('w')
+
+ expect(page).to have_active_navigation('Wiki')
+ end
+ end
+end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 2a316a0d0db..7f547a4ca1f 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'View on environment', js: true do
+describe 'View on environment', :js do
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 9a4ccf3c54d..78c350c8ee4 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Wiki > User previews markdown changes', js: true do
+feature 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_content) do
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 9d66f482c8d..e72b7dc0dd5 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -1,38 +1,75 @@
require 'spec_helper'
-feature 'Projects > Wiki > User creates wiki page', :js do
+describe 'User creates wiki page' do
let(:user) { create(:user) }
- background do
- project.team << [user, :master]
+ before do
+ project.add_master(user)
sign_in(user)
- visit project_path(project)
+ visit(project_wikis_path(project))
end
- context 'in the user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ context 'when wiki is empty' do
+ context 'in a user namespace' do
+ let(:project) { create(:project, namespace: user.namespace) }
- context 'when wiki is empty' do
- before do
- find('.shortcuts-wiki').trigger('click')
+ it 'shows validation error message' do
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: '')
+ click_on('Create page')
+ end
+
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content("Content can't be blank")
+
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: '[link test](test)')
+ click_on('Create page')
+ end
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content('link test')
+
+ click_link('link test')
+
+ expect(page).to have_content('Create Page')
+ end
+
+ it 'shows non-escaped link in the pages list', :js do
+ click_link('New page')
+
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'one/two/three-test')
+ click_on('Create page')
+ end
+
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
+
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end
- scenario 'commit message field has value "Create home"' do
+ it 'has "Create home" as a commit message' do
expect(page).to have_field('wiki[message]', with: 'Create home')
end
- scenario 'directly from the wiki home page' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- page.within '.wiki-form' do
- click_button 'Create page'
+ it 'creates a page from the home page' do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+
+ page.within('.wiki-form') do
+ click_button('Create page')
end
+
expect(page).to have_content('Home')
expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
- scenario 'creates ASCII wiki with LaTeX blocks' do
+ it 'creates ASCII wiki with LaTeX blocks', :js do
stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true)
ascii_content = <<~MD
@@ -54,10 +91,10 @@ feature 'Projects > Wiki > User creates wiki page', :js do
MD
find('#wiki_format option[value=asciidoc]').select_option
- fill_in :wiki_content, with: ascii_content
+ fill_in(:wiki_content, with: ascii_content)
- page.within '.wiki-form' do
- click_button 'Create page'
+ page.within('.wiki-form') do
+ click_button('Create page')
end
page.within '.wiki' do
@@ -67,27 +104,49 @@ feature 'Projects > Wiki > User creates wiki page', :js do
end
end
- context 'when wiki is not empty' do
- before do
- WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
- find('.shortcuts-wiki').trigger('click')
+ context 'in a group namespace', :js do
+ let(:project) { create(:project, namespace: create(:group, :public)) }
+
+ it 'has "Create home" as a commit message' do
+ expect(page).to have_field('wiki[message]', with: 'Create home')
+ end
+
+ it 'creates a page from from the home page' do
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Create page')
+ end
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
end
+ end
+ end
+
+ context 'when wiki is not empty', :js do
+ before do
+ create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' })
+ end
+
+ context 'in a user namespace' do
+ let(:project) { create(:project, namespace: user.namespace) }
context 'via the "new wiki page" page' do
- scenario 'when the wiki page has a single word name' do
- click_link 'New page'
+ it 'creates a page with a single word' do
+ click_link('New page')
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'foo'
- click_button 'Create page'
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'foo')
+ click_button('Create page')
end
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create foo')
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Create page')
end
expect(page).to have_content('Foo')
@@ -95,20 +154,20 @@ feature 'Projects > Wiki > User creates wiki page', :js do
expect(page).to have_content('My awesome wiki!')
end
- scenario 'when the wiki page has spaces in the name' do
- click_link 'New page'
+ it 'creates a page with spaces in the name' do
+ click_link('New page')
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'Spaces in the name'
- click_button 'Create page'
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'Spaces in the name')
+ click_button('Create page')
end
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create spaces in the name')
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Create page')
end
expect(page).to have_content('Spaces in the name')
@@ -116,20 +175,20 @@ feature 'Projects > Wiki > User creates wiki page', :js do
expect(page).to have_content('My awesome wiki!')
end
- scenario 'when the wiki page has hyphens in the name' do
- click_link 'New page'
+ it 'creates a page with hyphens in the name' do
+ click_link('New page')
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'hyphens-in-the-name'
- click_button 'Create page'
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'hyphens-in-the-name')
+ click_button('Create page')
end
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name')
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Create page')
end
expect(page).to have_content('Hyphens in the name')
@@ -138,73 +197,47 @@ feature 'Projects > Wiki > User creates wiki page', :js do
end
end
- scenario 'content has autocomplete' do
- click_link 'New page'
+ it 'shows the autocompletion dropdown' do
+ click_link('New page')
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'test-autocomplete'
- click_button 'Create page'
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'test-autocomplete')
+ click_button('Create page')
end
- page.within '.wiki-form' do
+ page.within('.wiki-form') do
find('#wiki_content').native.send_keys('')
- fill_in :wiki_content, with: '@'
+ fill_in(:wiki_content, with: '@')
end
expect(page).to have_selector('.atwho-view')
end
end
- end
-
- context 'in a group namespace' do
- let(:project) { create(:project, namespace: create(:group, :public)) }
- context 'when wiki is empty' do
- before do
- find('.shortcuts-wiki').trigger('click')
- end
-
- scenario 'commit message field has value "Create home"' do
- expect(page).to have_field('wiki[message]', with: 'Create home')
- end
+ context 'in a group namespace' do
+ let(:project) { create(:project, namespace: create(:group, :public)) }
- scenario 'directly from the wiki home page' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- page.within '.wiki-form' do
- click_button 'Create page'
- end
-
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
- end
- end
-
- context 'when wiki is not empty' do
- before do
- WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
- find('.shortcuts-wiki').trigger('click')
- end
+ context 'via the "new wiki page" page' do
+ it 'creates a page' do
+ click_link('New page')
- scenario 'via the "new wiki page" page' do
- click_link 'New page'
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'foo')
+ click_button('Create page')
+ end
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'foo'
- click_button 'Create page'
- end
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Create foo')
- # Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create foo')
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Create page')
+ end
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ expect(page).to have_content('Foo')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
end
-
- expect(page).to have_content('Foo')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
end
end
end
diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
new file mode 100644
index 00000000000..605e332196b
--- /dev/null
+++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+feature 'User deletes wiki page' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
+
+ before do
+ sign_in(user)
+ visit(project_wiki_path(project, wiki_page))
+ end
+
+ it 'deletes a page' do
+ click_on('Edit')
+ click_on('Delete')
+
+ expect(page).to have_content('Page was successfully deleted')
+ end
+end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 64a80aec205..949d90a50ff 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -1,83 +1,154 @@
require 'spec_helper'
-feature 'Projects > Wiki > User updates wiki page' do
+describe 'User updates wiki page' do
let(:user) { create(:user) }
- let!(:wiki_page) { WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute }
- background do
- project.team << [user, :master]
+ before do
+ project.add_master(user)
sign_in(user)
+ end
+
+ context 'when wiki is empty' do
+ before do
+ visit(project_wikis_path(project))
+ end
+
+ context 'in a user namespace' do
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'redirects back to the home edit page' do
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
+ end
+
+ expect(current_path).to eq project_wiki_path(project, :home)
+ end
+
+ it 'updates a page that has a path', :js do
+ click_on('New page')
+
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'one/two/three-test')
+ click_on('Create page')
+ end
+
+ page.within '.wiki-form' do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
- visit project_wikis_path(project)
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('Three')
+
+ first(:link, text: 'Three').click
+
+ expect(find('.nav-text')).to have_content('Three')
+
+ click_on('Edit')
+
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
+
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
+
+ expect(page).to have_content('Updated Wiki Content')
+ end
+ end
end
- context 'in the user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ context 'when wiki is not empty' do
+ let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) }
- context 'the home page' do
- scenario 'success when the wiki content is not empty' do
- click_link 'Edit'
+ before do
+ visit(project_wikis_path(project))
+ end
+
+ context 'in a user namespace' do
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'updates a page' do
+ click_link('Edit')
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Save changes'
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Save changes')
expect(page).to have_content('Home')
expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
- scenario 'failure when the wiki content is empty' do
- click_link 'Edit'
+ it 'shows a validation error message' do
+ click_link('Edit')
- fill_in :wiki_content, with: ''
- click_button 'Save changes'
+ fill_in(:wiki_content, with: '')
+ click_button('Save changes')
expect(page).to have_selector('.wiki-form')
expect(page).to have_content('Edit Page')
expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content('Content can\'t be blank')
- expect(find('textarea#wiki_content').value).to eq ''
+ expect(page).to have_content("Content can't be blank")
+ expect(find('textarea#wiki_content').value).to eq('')
end
- scenario 'content has autocomplete', :js do
- click_link 'Edit'
+ it 'shows the autocompletion dropdown', :js do
+ click_link('Edit')
find('#wiki_content').native.send_keys('')
- fill_in :wiki_content, with: '@'
+ fill_in(:wiki_content, with: '@')
expect(page).to have_selector('.atwho-view')
end
- end
- scenario 'page has been updated since the user opened the edit page' do
- click_link 'Edit'
+ it 'shows the error message' do
+ click_link('Edit')
+
+ wiki_page.update(content: 'Update')
- wiki_page.update(content: 'Update')
+ click_button('Save changes')
+
+ expect(page).to have_content('Someone edited the page the same time you did.')
+ end
+
+ it 'updates a page' do
+ click_on('Edit')
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
+
+ expect(page).to have_content('Updated Wiki Content')
+ end
- click_button 'Save changes'
+ it 'cancels edititng of a page' do
+ click_on('Edit')
- expect(page).to have_content 'Someone edited the page the same time you did.'
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
+ end
+
+ expect(current_path).to eq(project_wiki_path(project, wiki_page))
+ end
end
- end
- context 'in a group namespace' do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ context 'in a group namespace' do
+ let(:project) { create(:project, namespace: create(:group, :public)) }
- scenario 'the home page' do
- click_link 'Edit'
+ it 'updates a page' do
+ click_link('Edit')
- # Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Update home')
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Save changes'
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Save changes')
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
end
end
end
diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
deleted file mode 100644
index 92e96f11219..00000000000
--- a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-feature 'Projects > Wiki > User views the wiki page' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:old_page_version_id) { wiki_page.versions.last.id }
- let(:wiki_page) do
- WikiPages::CreateService.new(
- project,
- user,
- title: 'home',
- content: '[some link](other-page)'
- ).execute
- end
-
- background do
- project.team << [user, :master]
- sign_in(user)
- WikiPages::UpdateService.new(
- project,
- user,
- message: 'updated home',
- content: 'updated [some link](other-page)',
- format: :markdown
- ).execute(wiki_page)
- end
-
- scenario 'Visit Wiki Page Current Commit' do
- visit project_wiki_path(project, wiki_page)
-
- expect(page).to have_selector('a.btn', text: 'Edit')
- end
-
- scenario 'Visit Wiki Page Historical Commit' do
- visit project_wiki_path(project, wiki_page, version_id: old_page_version_id)
-
- expect(page).not_to have_selector('a.btn', text: 'Edit')
- end
-end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
new file mode 100644
index 00000000000..470391dc66b
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'User views a wiki page' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:wiki_page) do
+ create(:wiki_page,
+ wiki: project.wiki,
+ attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' })
+ end
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when wiki is empty' do
+ before do
+ visit(project_wikis_path(project))
+
+ click_on('New page')
+
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'one/two/three-test')
+ click_on('Create page')
+ end
+
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
+ end
+
+ it 'shows the history of a page that has a path', :js do
+ expect(current_path).to include('one/two/three-test')
+
+ first(:link, text: 'Three').click
+ click_on('Page history')
+
+ expect(current_path).to include('one/two/three-test')
+
+ page.within(:css, '.nav-text') do
+ expect(page).to have_content('History')
+ end
+ end
+
+ it 'shows an old version of a page', :js do
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('Three')
+
+ first(:link, text: 'Three').click
+
+ expect(find('.nav-text')).to have_content('Three')
+
+ click_on('Edit')
+
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
+
+ fill_in('Content', with: 'Updated Wiki Content')
+
+ click_on('Save changes')
+ click_on('Page history')
+
+ page.within(:css, '.nav-text') do
+ expect(page).to have_content('History')
+ end
+
+ find('a[href*="?version_id"]')
+ end
+ end
+
+ context 'when a page does not have history' do
+ before do
+ visit(project_wiki_path(project, wiki_page))
+ end
+
+ it 'shows all the pages' do
+ expect(page).to have_content(user.name)
+ expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
+ end
+
+ it 'shows a file stored in a page' do
+ file = Gollum::File.new(project.wiki)
+
+ allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file)
+ allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg')
+
+ expect(page).to have_xpath('//img[@data-src="image.jpg"]')
+ expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
+
+ click_on('image')
+
+ expect(current_path).to match('wikis/image.jpg')
+ expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
+ end
+
+ it 'shows the creation page if file does not exist' do
+ expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
+
+ click_on('image')
+
+ expect(current_path).to match('wikis/image.jpg')
+ expect(page).to have_content('New Wiki Page')
+ expect(page).to have_content('Create page')
+ end
+ end
+
+ context 'when a page has history' do
+ before do
+ wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
+ end
+
+ it 'shows the page history' do
+ visit(project_wiki_path(project, wiki_page))
+
+ expect(page).to have_selector('a.btn', text: 'Edit')
+
+ click_on('Page history')
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content("#{user.username} created page: home")
+ expect(page).to have_content('updated home')
+ end
+
+ it 'does not show the "Edit" button' do
+ visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id))
+
+ expect(page).not_to have_selector('a.btn', text: 'Edit')
+ end
+ end
+
+ it 'opens a default wiki page', :js do
+ visit(project_path(project))
+
+ find('.shortcuts-wiki').trigger('click')
+
+ expect(page).to have_content('Home · Create Page')
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 81f7ab80a04..ac62280e4ca 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Project' do
+ include ProjectForksHelper
+
describe 'creating from template' do
let(:user) { create(:user) }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
@@ -55,13 +57,12 @@ feature 'Project' do
end
end
- describe 'remove forked relationship', js: true do
+ describe 'remove forked relationship', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) }
before do
sign_in user
- create(:forked_project_link, forked_to_project: project)
visit edit_project_path(project)
end
@@ -71,12 +72,61 @@ feature 'Project' do
remove_with_confirm('Remove fork relationship', project.path)
expect(page).to have_content 'The fork relationship has been removed.'
- expect(project.forked?).to be_falsey
+ expect(project.reload.forked?).to be_falsey
expect(page).not_to have_content 'Remove fork relationship'
end
end
- describe 'removal', js: true do
+ describe 'showing information about source of a project fork' do
+ let(:user) { create(:user) }
+ let(:base_project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(base_project, user, repository: true) }
+
+ before do
+ sign_in user
+ end
+
+ it 'shows a link to the source project when it is available' do
+ visit project_path(forked_project)
+
+ expect(page).to have_content('Forked from')
+ expect(page).to have_link(base_project.full_name)
+ end
+
+ it 'does not contain fork network information for the root project' do
+ forked_project
+
+ visit project_path(base_project)
+
+ expect(page).not_to have_content('In fork network of')
+ expect(page).not_to have_content('Forked from')
+ end
+
+ it 'shows the name of the deleted project when the source was deleted' do
+ forked_project
+ Projects::DestroyService.new(base_project, base_project.owner).execute
+
+ visit project_path(forked_project)
+
+ expect(page).to have_content("Forked from #{base_project.full_name} (deleted)")
+ end
+
+ context 'a fork of a fork' do
+ let(:fork_of_fork) { fork_project(forked_project, user, repository: true) }
+
+ it 'links to the base project if the source project is removed' do
+ fork_of_fork
+ Projects::DestroyService.new(forked_project, user).execute
+
+ visit project_path(fork_of_fork)
+
+ expect(page).to have_content("Forked from")
+ expect(page).to have_link(base_project.full_name)
+ end
+ end
+ end
+
+ describe 'removal', :js do
let(:user) { create(:user, username: 'test', name: 'test') }
let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 3677bf38724..2ab1eda90f1 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,93 +1,178 @@
require 'spec_helper'
-feature 'Protected Branches', js: true do
- let(:user) { create(:user, :admin) }
+feature 'Protected Branches', :js do
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
- before do
- sign_in(user)
- end
+ context 'logged in as developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").trigger('click')
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
+ describe 'Delete protected branch' do
+ before do
+ create(:protected_branch, project: project, name: 'fix')
+ expect(ProtectedBranch.count).to eq(1)
+ end
+
+ it 'does not allow developer to removes protected branch' do
+ visit project_branches_path(project)
- describe "explicit protected branches" do
- it "allows creating explicit protected branches" do
- visit project_protected_branches_path(project)
- set_protected_branch_name('some-branch')
- click_on "Protect"
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
- within(".protected-branches-list") { expect(page).to have_content('some-branch') }
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.name).to eq('some-branch')
+ expect(page).to have_css('.btn-remove.disabled')
+ end
end
+ end
- it "displays the last commit on the matching branch if it exists" do
- commit = create(:commit, project: project)
- project.repository.add_branch(user, 'some-branch', commit.id)
+ context 'logged in as master' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- visit project_protected_branches_path(project)
- set_protected_branch_name('some-branch')
- click_on "Protect"
+ describe 'Delete protected branch' do
+ before do
+ create(:protected_branch, project: project, name: 'fix')
+ expect(ProtectedBranch.count).to eq(1)
+ end
- within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
- end
+ it 'removes branch after modal confirmation' do
+ visit project_branches_path(project)
+
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ page.find('[data-target="#modal-delete-branch"]').trigger(:click)
- it "displays an error message if the named branch does not exist" do
- visit project_protected_branches_path(project)
- set_protected_branch_name('some-branch')
- click_on "Protect"
+ expect(page).to have_css('.js-delete-branch[disabled]')
+ fill_in 'delete_branch_input', with: 'fix'
+ click_link 'Delete protected branch'
- within(".protected-branches-list") { expect(page).to have_content('branch was removed') }
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('No branches to show')
+ end
end
- end
- describe "wildcard protected branches" do
- it "allows creating protected branches with a wildcard" do
- visit project_protected_branches_path(project)
- set_protected_branch_name('*-stable')
- click_on "Protect"
+ describe "Saved defaults" do
+ it "keeps the allowed to merge and push dropdowns defaults based on the previous selection" do
+ visit project_protected_branches_path(project)
+ form = '.js-new-protected-branch'
+
+ within form do
+ find(".js-allowed-to-merge").trigger('click')
+ click_link 'No one'
+ find(".js-allowed-to-push").trigger('click')
+ click_link 'Developers + Masters'
+ end
+
+ visit project_protected_branches_path(project)
+
+ within form do
+ page.within(".js-allowed-to-merge") do
+ expect(page.find(".dropdown-toggle-text")).to have_content("No one")
+ end
+ page.within(".js-allowed-to-push") do
+ expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters")
+ end
+ end
+ end
+ end
+ end
- within(".protected-branches-list") { expect(page).to have_content('*-stable') }
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.name).to eq('*-stable')
+ context 'logged in as admin' do
+ before do
+ sign_in(admin)
end
- it "displays the number of matching branches" do
- project.repository.add_branch(user, 'production-stable', 'master')
- project.repository.add_branch(user, 'staging-stable', 'master')
+ describe "explicit protected branches" do
+ it "allows creating explicit protected branches" do
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('some-branch') }
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.name).to eq('some-branch')
+ end
+
+ it "displays the last commit on the matching branch if it exists" do
+ commit = create(:commit, project: project)
+ project.repository.add_branch(admin, 'some-branch', commit.id)
+
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
- visit project_protected_branches_path(project)
- set_protected_branch_name('*-stable')
- click_on "Protect"
+ within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
+ end
- within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
+ it "displays an error message if the named branch does not exist" do
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('branch was removed') }
+ end
end
- it "displays all the branches matching the wildcard" do
- project.repository.add_branch(user, 'production-stable', 'master')
- project.repository.add_branch(user, 'staging-stable', 'master')
- project.repository.add_branch(user, 'development', 'master')
+ describe "wildcard protected branches" do
+ it "allows creating protected branches with a wildcard" do
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
- visit project_protected_branches_path(project)
- set_protected_branch_name('*-stable')
- click_on "Protect"
+ within(".protected-branches-list") { expect(page).to have_content('*-stable') }
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.name).to eq('*-stable')
+ end
- visit project_protected_branches_path(project)
- click_on "2 matching branches"
+ it "displays the number of matching branches" do
+ project.repository.add_branch(admin, 'production-stable', 'master')
+ project.repository.add_branch(admin, 'staging-stable', 'master')
- within(".protected-branches-list") do
- expect(page).to have_content("production-stable")
- expect(page).to have_content("staging-stable")
- expect(page).not_to have_content("development")
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
end
+
+ it "displays all the branches matching the wildcard" do
+ project.repository.add_branch(admin, 'production-stable', 'master')
+ project.repository.add_branch(admin, 'staging-stable', 'master')
+ project.repository.add_branch(admin, 'development', 'master')
+
+ visit project_protected_branches_path(project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
+
+ visit project_protected_branches_path(project)
+ click_on "2 matching branches"
+
+ within(".protected-branches-list") do
+ expect(page).to have_content("production-stable")
+ expect(page).to have_content("staging-stable")
+ expect(page).not_to have_content("development")
+ end
+ end
+ end
+
+ describe "access control" do
+ include_examples "protected branches > access control > CE"
end
end
- describe "access control" do
- include_examples "protected branches > access control > CE"
+ def set_protected_branch_name(branch_name)
+ find(".js-protected-branch-select").trigger('click')
+ find(".dropdown-input-field").set(branch_name)
+ click_on("Create wildcard #{branch_name}")
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 8abd4403065..8cc6f17b8d9 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Protected Tags', js: true do
+feature 'Protected Tags', :js do
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
new file mode 100644
index 00000000000..0ed797a62ea
--- /dev/null
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'User searches for code' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'finds a file' do
+ visit(project_path(project))
+
+ page.within('.search') do
+ fill_in('search', with: 'application.js')
+ click_button('Go')
+ end
+
+ click_link('Code')
+
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ end
+
+ context 'when on a project page', :js do
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds code' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'rspec')
+ find('.btn-search').trigger('click')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ it 'finds code' do
+ fill_in('search', with: 'rspec')
+ click_button('Go')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
new file mode 100644
index 00000000000..c7c469a262c
--- /dev/null
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'User searches for comments' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+
+ it 'finds a commit' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_text('Commit deleted')
+ expect(page).to have_text('12345678')
+ end
+ end
+ end
+
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+
+ it 'finds a snippet' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_link(snippet.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
new file mode 100644
index 00000000000..28cae444588
--- /dev/null
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'User searches for commits' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(search_path(project_id: project.id))
+ end
+
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ fill_in('search', with: sha)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+
+ it 'finds a commit in uppercase and redirects to its page' do
+ fill_in('search', with: sha.upcase)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+ end
+
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ create_commit('Message referencing another sha: "deadbeef"', project, user, 'master')
+
+ fill_in('search', with: 'deadbeef')
+ click_button('Search')
+
+ expect(page).to have_current_path('/search', only_path: true)
+ end
+
+ it 'finds multiple commits' do
+ fill_in('search', with: 'See merge request')
+ click_button('Search')
+ click_link('Commits')
+
+ expect(page).to have_selector('.commit-row-description', count: 9)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
new file mode 100644
index 00000000000..630a81b1c5e
--- /dev/null
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe 'User searches for issues', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:issue1) { create(:issue, title: 'Foo', project: project) }
+ let!(:issue2) { create(:issue, title: 'Bar', project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
new file mode 100644
index 00000000000..116256682f4
--- /dev/null
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for merge requests', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) }
+ let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a merge request' do
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
new file mode 100644
index 00000000000..4fa9fe9ce8c
--- /dev/null
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for milestones', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a milestone' do
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a milestone' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
new file mode 100644
index 00000000000..242e437e41c
--- /dev/null
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'User searches for projects' do
+ let!(:project) { create(:project, :public, name: 'Shop') }
+
+ context 'when signed out' do
+ include_examples 'top right search form'
+
+ it 'finds a project' do
+ visit(search_path)
+
+ fill_in('dashboard_search', with: project.name[0..3])
+ click_button('Search')
+
+ expect(page).to have_link(project.name)
+ end
+
+ it 'preserves the group being searched in' do
+ visit(search_path(group_id: project.namespace.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
+ end
+
+ it 'preserves the project being searched in' do
+ visit(search_path(project_id: project.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
new file mode 100644
index 00000000000..1ea56479ecc
--- /dev/null
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'User searches for wiki pages', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a page' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
new file mode 100644
index 00000000000..5ddea36add5
--- /dev/null
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe 'User uses header search field' do
+ include FilteredSearchHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ it 'starts searching by pressing the enter key', :js do
+ fill_in('search', with: 'gitlab')
+ find('#search').native.send_keys(:enter)
+
+ page.within('.breadcrumbs-sub-title') do
+ expect(page).to have_content('Search')
+ end
+ end
+
+ it 'contains location badge' do
+ expect(page).to have_selector('.has-location-badge')
+ end
+
+ context 'when clicking the search field', :js do
+ before do
+ page.find('#search').click
+ end
+
+ it 'shows category search dropdown' do
+ expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+
+ context 'when clicking issues' do
+ let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+
+ it 'shows assigned issues' do
+ find('.dropdown-menu').click_link('Issues assigned to me')
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created issues' do
+ find('.dropdown-menu').click_link("Issues I've created")
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ context 'when clicking merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+
+ it 'shows assigned merge requests' do
+ find('.dropdown-menu').click_link('Merge requests assigned to me')
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created merge requests' do
+ find('.dropdown-menu').click_link("Merge requests I've created")
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+ end
+
+ context 'when entering text into the search field', :js do
+ before do
+ page.within('.search-input-wrap') do
+ fill_in('search', with: project.name[0..3])
+ end
+ end
+
+ it 'does not display the category search dropdown' do
+ expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
new file mode 100644
index 00000000000..95f3eb5e805
--- /dev/null
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'User uses search filters', :js do
+ let(:group) { create(:group) }
+ let!(:group_project) { create(:project, group: group) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ group.add_owner(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ context' when filtering by group' do
+ it 'shows group projects' do
+ find('.js-search-group-dropdown').trigger('click')
+
+ wait_for_requests
+
+ page.within('.search-holder') do
+ click_link(group.name)
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ expect(page).to have_link(group_project.name_with_namespace)
+ end
+ end
+ end
+
+ context' when filtering by project' do
+ it 'shows a project' do
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ click_link(project.name_with_namespace)
+ end
+
+ expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ end
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
deleted file mode 100644
index 8f6d0bb9d1b..00000000000
--- a/spec/features/search_spec.rb
+++ /dev/null
@@ -1,310 +0,0 @@
-require 'spec_helper'
-
-describe "Search" do
- include FilteredSearchHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
- let!(:issue2) { create(:issue, project: project, author: user) }
-
- before do
- sign_in(user)
- project.team << [user, :reporter]
- visit search_path
- end
-
- it 'does not show top right search form' do
- expect(page).not_to have_selector('.search')
- end
-
- context 'search filters', js: true do
- let(:group) { create(:group) }
- let!(:group_project) { create(:project, group: group) }
-
- before do
- group.add_owner(user)
- end
-
- it 'shows group name after filtering' do
- find('.js-search-group-dropdown').trigger('click')
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
- end
-
- it 'filters by group projects after filtering by group' do
- find('.js-search-group-dropdown').trigger('click')
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
-
- page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
- wait_for_requests
-
- expect(page).to have_link(group_project.name_with_namespace)
- end
- end
-
- it 'shows project name after filtering' do
- page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
- wait_for_requests
-
- click_link project.name_with_namespace
- end
-
- expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
- end
- end
-
- describe 'searching for Projects' do
- it 'finds a project' do
- page.within '.search-holder' do
- fill_in "search", with: project.name[0..3]
- click_button "Search"
- end
-
- expect(page).to have_content project.name
- end
- end
-
- 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 do
- note.update_attributes(commit_id: 12345678)
- end
-
- it 'finds comment' do
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_text("Commit deleted")
- expect(page).to have_text("12345678")
- end
- end
-
- it 'finds a snippet' do
- snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
- note = create(:note,
- noteable: snippet,
- author: user,
- note: 'Supercalifragilisticexpialidocious',
- project: project)
- # Must visit project dashboard since global search won't search
- # everything (e.g. comments, snippets, etc.)
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_link(snippet.title)
- end
-
- it 'finds a commit' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'add'
- click_button 'Go'
- end
-
- click_link "Commits"
-
- expect(page).to have_selector('.commit-row-description')
- end
-
- it 'finds a code' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'application.js'
- click_button 'Go'
- end
-
- click_link "Code"
-
- expect(page).to have_selector('.file-content .code')
-
- expect(page).to have_selector("span.line[lang='javascript']")
- end
- end
-
- describe 'Right header search field' do
- it 'allows enter key to search', js: true do
- visit project_path(project)
- fill_in 'search', with: 'gitlab'
- find('#search').native.send_keys(:enter)
-
- page.within '.breadcrumbs-sub-title' do
- expect(page).to have_content 'Search'
- end
- end
-
- describe 'Search in project page' do
- before do
- visit project_path(project)
- end
-
- it 'shows top right search form' do
- expect(page).to have_selector('#search')
- end
-
- it 'contains location badge in top right search form' do
- expect(page).to have_selector('.has-location-badge')
- end
-
- context 'clicking the search field', js: true do
- it 'shows category search dropdown' do
- page.find('#search').click
-
- expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
-
- context 'click the links in the category search dropdown', js: true do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
-
- before do
- page.find('#search').click
- end
-
- it 'takes user to her issues page when issues assigned is clicked' do
- find('.dropdown-menu').click_link 'Issues assigned to me'
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her issues page when issues authored is clicked' do
- find('.dropdown-menu').click_link "Issues I've created"
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR assigned is clicked' do
- find('.dropdown-menu').click_link 'Merge requests assigned to me'
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR authored is clicked' do
- find('.dropdown-menu').click_link "Merge requests I've created"
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
- end
-
- context 'entering text into the search field', js: true do
- before do
- page.within '.search-input-wrap' do
- fill_in "search", with: project.name[0..3]
- end
- end
-
- it 'does not display the category search dropdown' do
- expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
- end
- end
-
- describe 'search for commits' do
- let(:project) { create(:project, :repository) }
-
- before do
- visit search_path(project_id: project.id)
- end
-
- it 'redirects to commit page when search by sha and only commit found' do
- fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'redirects to single commit regardless of query case' do
- fill_in 'search', with: '6D394385cf'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'holds on /search page when the only commit is found by message' do
- create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
-
- fill_in 'search', with: 'deadbeef'
- click_button 'Search'
-
- expect(page).to have_current_path('/search', only_path: true)
- end
-
- it 'shows multiple matching commits' do
- fill_in 'search', with: 'See merge request'
-
- click_button 'Search'
- click_link 'Commits'
-
- expect(page).to have_selector('.commit-row-description', count: 9)
- end
- end
-
- context 'anonymous user' do
- let(:project) { create(:project, :public) }
-
- before do
- sign_out(user)
- end
-
- it 'preserves the group being searched in' do
- visit search_path(group_id: project.namespace.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
- end
-
- it 'preserves the project being searched in' do
- visit search_path(project_id: project.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
- end
- end
-end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index a7928857b7d..d70cf1527e7 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -181,21 +181,6 @@ describe "Internal Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/issues/:id/edit" do
- let(:issue) { create(:issue, project: project) }
- subject { edit_project_issue_path(project, issue) }
-
- 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
-
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a4396b20afd..ea130606545 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -181,21 +181,6 @@ describe "Private Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/issues/:id/edit" do
- let(:issue) { create(:issue, project: project) }
- subject { edit_project_issue_path(project, issue) }
-
- 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
-
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index fccdeb0e5b7..d15f5af66c9 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -394,21 +394,6 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:visitor) }
end
- describe "GET /:project_path/issues/:id/edit" do
- let(:issue) { create(:issue, project: project) }
- subject { edit_project_issue_path(project, issue) }
-
- 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
-
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index b6367b88e17..917fad74ef1 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -24,6 +24,24 @@ feature 'Signup' do
end
end
+ context "when sigining up with different cased emails" do
+ it "creates the user successfully" do
+ user = build(:user)
+
+ visit root_path
+
+ fill_in 'new_user_name', with: user.name
+ fill_in 'new_user_username', with: user.username
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_email_confirmation', with: user.email.capitalize
+ fill_in 'new_user_password', with: user.password
+ click_button "Register"
+
+ expect(current_path).to eq dashboard_projects_path
+ expect(page).to have_content("Welcome! You have signed up successfully.")
+ end
+ end
+
context "when not sending confirmation email" do
before do
stub_application_setting(send_user_confirmation_email: false)
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index 3a229612235..3a2768c424f 100644
--- a/spec/features/snippets/internal_snippet_spec.rb
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Internal Snippets', js: true do
+feature 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) }
describe 'normal user' do
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 39d79a3327b..1455345bd56 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -55,7 +55,7 @@ feature 'Master creates tag' do
end
end
- scenario 'opens dropdown for ref', js: true do
+ scenario 'opens dropdown for ref', :js do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index d6a6b8fc7d5..f5b3774122b 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,7 +10,7 @@ feature 'Master deletes tag' do
visit project_tags_path(project)
end
- context 'from the tags list page', js: true do
+ context 'from the tags list page', :js do
scenario 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
@@ -34,16 +34,31 @@ feature 'Master deletes tag' do
end
end
- context 'when pre-receive hook fails', js: true do
- before do
- allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
+ context 'when pre-receive hook fails', :js do
+ context 'when Gitaly operation_user_delete_tag feature is enabled' do
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
+ .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
+ end
+
+ scenario 'shows the error message' do
+ delete_first_tag
+
+ expect(page).to have_content('Do not delete tags')
+ end
end
- scenario 'shows the error message' do
- delete_first_tag
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
+ before do
+ allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
+ .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
+ end
+
+ scenario 'shows the error message' do
+ delete_first_tag
- expect(page).to have_content('Do not delete tags')
+ expect(page).to have_content('Do not delete tags')
+ end
end
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index aeb0534b733..485b0b287ad 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -63,7 +63,7 @@ feature 'Task Lists' do
end
describe 'for Issues' do
- describe 'multiple tasks', js: true do
+ describe 'multiple tasks', :js do
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
it 'renders' do
@@ -103,7 +103,7 @@ feature 'Task Lists' do
end
end
- describe 'single incomplete task', js: true do
+ describe 'single incomplete task', :js do
let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -122,7 +122,7 @@ feature 'Task Lists' do
end
end
- describe 'single complete task', js: true do
+ describe 'single complete task', :js do
let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -141,7 +141,7 @@ feature 'Task Lists' do
end
end
- describe 'nested tasks', js: true do
+ describe 'nested tasks', :js do
let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
before do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 47664de469a..548d8372a07 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Triggers', js: true do
+feature 'Triggers', :js do
let(:trigger_title) { 'trigger desc' }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index e1c95590af1..1261ffdc2ee 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -14,20 +14,20 @@ feature 'User uploads file to note' do
end
context 'before uploading' do
- it 'shows "Attach a file" button', js: true do
+ it 'shows "Attach a file" button', :js do
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'uploading is in progress' do
- it 'shows "Cancel" button on uploading', js: true do
+ it 'shows "Cancel" button on uploading', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_button('Cancel')
end
- it 'cancels uploading on clicking to "Cancel" button', js: true do
+ it 'cancels uploading on clicking to "Cancel" button', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
click_button 'Cancel'
@@ -37,20 +37,20 @@ feature 'User uploads file to note' do
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- it 'shows "Attaching a file" message on uploading 1 file', js: true do
+ it 'shows "Attaching a file" message on uploading 1 file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
end
- it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
+ it 'shows "Attaching 2 files" message on uploading 2 file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
end
- it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
+ it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
@@ -63,7 +63,7 @@ feature 'User uploads file to note' do
end
context 'uploading is complete' do
- it 'shows "Attach a file" button on uploading complete', js: true do
+ it 'shows "Attach a file" button on uploading complete', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
wait_for_requests
@@ -71,7 +71,7 @@ feature 'User uploads file to note' do
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- scenario 'they see the attached file', js: true do
+ scenario 'they see the attached file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
deleted file mode 100644
index 37d66b618af..00000000000
--- a/spec/features/user_callout_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe 'User Callouts', js: true do
- let(:user) { create(:user) }
- let(:another_user) { create(:user) }
- let(:project) { create(:project, path: 'gitlab', name: 'sample') }
-
- before do
- sign_in(user)
- project.team << [user, :master]
- end
-
- it 'takes you to the profile preferences when the link is clicked' do
- visit dashboard_projects_path
- click_link 'Check it out'
- expect(current_path).to eq profile_preferences_path
- end
-
- it 'does not show when cookie is set' do
- visit dashboard_projects_path
-
- within('.user-callout') do
- find('.close').trigger('click')
- end
-
- visit dashboard_projects_path
-
- expect(page).not_to have_selector('.user-callout')
- end
-
- describe 'user callout should appear in two routes' do
- it 'shows up on the user profile' do
- visit user_path(user)
- expect(find('.user-callout')).to have_content 'Customize your experience'
- end
-
- it 'shows up on the dashboard projects' do
- visit dashboard_projects_path
- expect(find('.user-callout')).to have_content 'Customize your experience'
- end
- end
-
- it 'hides the user callout when click on the dismiss icon' do
- visit user_path(user)
- within('.user-callout') do
- find('.close').click
- end
- expect(page).not_to have_selector('.user-callout')
- end
-
- it 'does not show callout on another users profile' do
- visit user_path(another_user)
- expect(page).not_to have_selector('.user-callout')
- end
-end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 13760b4c2fc..8c697e33436 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Snippets tab on a user profile', js: true do
+describe 'Snippets tab on a user profile', :js do
context 'when the user has snippets' do
let(:user) { create(:user) }
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 15b89dac572..0252c957c95 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Users', js: true do
+feature 'Users', :js do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 6794bf4f4ba..5d8e818f7bf 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project variables', js: true do
+describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
new file mode 100644
index 00000000000..684af74d750
--- /dev/null
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe AutocompleteUsersFinder do
+ describe '#execute' do
+ let!(:user1) { create(:user, username: 'johndoe') }
+ let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let!(:external_user) { create(:user, :external) }
+ let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let(:current_user) { create(:user) }
+ let(:params) { {} }
+
+ let(:project) { nil }
+ let(:group) { nil }
+
+ subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a }
+
+ context 'when current_user not passed or nil' do
+ let(:current_user) { nil }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when project passed' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to match_array([project.owner]) }
+
+ context 'when author_id passed' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([project.owner, user2]) }
+ end
+ end
+
+ context 'when group passed and project not passed' do
+ let(:group) { create(:group, :public) }
+
+ before do
+ group.add_users([user1], GroupMember::DEVELOPER)
+ end
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
+
+ context 'when filtered by search' do
+ let(:params) { { search: 'johndoe' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by skip_users' do
+ let(:params) { { skip_users: [omniauth_user.id, current_user.id] } }
+
+ it { is_expected.to match_array([user1, external_user]) }
+ end
+
+ context 'when todos exist' do
+ let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) }
+ let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) }
+ let!(:done_todo1) { create(:todo, user: current_user, author: external_user, state: :done) }
+ let!(:done_todo2) { create(:todo, user: user1, author: external_user, state: :done) }
+
+ context 'when filtered by todo_filter without todo_state_filter' do
+ let(:params) { { todo_filter: true } }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when filtered by todo_filter with pending todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'pending' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by todo_filter with done todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'done' } }
+
+ it { is_expected.to match_array([external_user]) }
+ end
+ end
+
+ context 'when filtered by current_user' do
+ let(:current_user) { user2 }
+ let(:params) { { current_user: true } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) }
+ end
+
+ context 'when filtered by author_id' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) }
+ end
+ end
+end
diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb
new file mode 100644
index 00000000000..f0cef7ea406
--- /dev/null
+++ b/spec/finders/fork_projects_finder_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ForkProjectsFinder do
+ let(:source_project) { create(:project, :empty_repo) }
+ let(:private_fork) { create(:project, :private, :empty_repo, name: 'A') }
+ let(:internal_fork) { create(:project, :internal, :empty_repo, name: 'B') }
+ let(:public_fork) { create(:project, :public, :empty_repo, name: 'C') }
+
+ let(:non_member) { create(:user) }
+ let(:private_fork_member) { create(:user) }
+
+ before do
+ private_fork.add_developer(private_fork_member)
+
+ source_project.forks << private_fork
+ source_project.forks << internal_fork
+ source_project.forks << public_fork
+ end
+
+ describe '#execute' do
+ let(:finder) { described_class.new(source_project, params: {}, current_user: current_user) }
+
+ subject { finder.execute }
+
+ describe 'without a user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to eq([public_fork]) }
+ end
+
+ describe 'with a user' do
+ let(:current_user) { non_member }
+
+ it { is_expected.to eq([public_fork, internal_fork]) }
+ end
+
+ describe 'with a member' do
+ let(:current_user) { private_fork_member }
+
+ it { is_expected.to eq([public_fork, internal_fork, private_fork]) }
+ end
+ end
+end
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
new file mode 100644
index 00000000000..c81bfd7932c
--- /dev/null
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe MergeRequestTargetProjectFinder do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ subject(:finder) { described_class.new(current_user: user, source_project: forked_project) }
+
+ shared_examples 'finding related projects' do
+ it 'finds sibling projects and base project' do
+ other_fork
+
+ expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project)
+ end
+
+ it 'does not include projects that have merge requests turned off' do
+ other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+ base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+
+ expect(finder.execute).to contain_exactly(forked_project)
+ end
+ end
+
+ context 'public projects' do
+ let(:base_project) { create(:project, :public, path: 'base') }
+ let(:forked_project) { fork_project(base_project) }
+ let(:other_fork) { fork_project(base_project) }
+
+ it_behaves_like 'finding related projects'
+ end
+
+ context 'private projects' do
+ let(:base_project) { create(:project, :private, path: 'base') }
+ let(:forked_project) { fork_project(base_project, base_project.owner) }
+ let(:other_fork) { fork_project(base_project, base_project.owner) }
+
+ context 'when the user is a member of all projects' do
+ before do
+ base_project.add_developer(user)
+ forked_project.add_developer(user)
+ other_fork.add_developer(user)
+ end
+
+ it_behaves_like 'finding related projects'
+ end
+
+ it 'only finds the projects the user is a member of' do
+ other_fork.add_developer(user)
+ base_project.add_developer(user)
+
+ expect(finder.execute).to contain_exactly(other_fork, base_project)
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 95f445e7905..883bdf3746a 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -1,12 +1,18 @@
require 'spec_helper'
describe MergeRequestsFinder do
+ include ProjectForksHelper
+
let(:user) { create :user }
let(:user2) { create :user }
- let(:project1) { create(:project) }
- let(:project2) { create(:project, forked_from_project: project1) }
- let(:project3) { create(:project, :archived, forked_from_project: project1) }
+ let(:project1) { create(:project, :public) }
+ let(:project2) { fork_project(project1, user) }
+ let(:project3) do
+ p = fork_project(project1, user)
+ p.update!(archived: true)
+ p
+ end
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index 1bab6d64388..4249c52c481 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -56,6 +56,15 @@ describe UsersFinder do
expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username])
end
+
+ it 'does not filter by custom attributes' do
+ users = described_class.new(
+ user,
+ custom_attributes: { foo: 'bar' }
+ ).execute
+
+ expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ end
end
context 'with an admin user' do
@@ -72,6 +81,19 @@ describe UsersFinder do
expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
end
+
+ it 'filters by custom attributes' do
+ create :user_custom_attribute, user: user1, key: 'foo', value: 'foo'
+ create :user_custom_attribute, user: user1, key: 'bar', value: 'bar'
+ create :user_custom_attribute, user: user2, key: 'foo', value: 'foo'
+
+ users = described_class.new(
+ admin,
+ custom_attributes: { foo: 'foo', bar: 'bar' }
+ ).execute
+
+ expect(users).to contain_exactly(user1)
+ end
end
end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
new file mode 100644
index 00000000000..1f255a17881
--- /dev/null
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required" : [
+ "status"
+ ],
+ "properties" : {
+ "status": { "type": "string" },
+ "status_reason": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 1030f323a1f..ba094ba1657 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -46,6 +46,7 @@
"branch_missing": { "type": "boolean" },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
+ "mergeable": { "type": "boolean" },
"project_archived": { "type": "boolean" },
"only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
"has_ci": { "type": "boolean" },
@@ -93,10 +94,12 @@
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
- "remove_wip_path": { "type": "string" },
+ "remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
- "merge_ongoing": { "type": "boolean" }
+ "merge_ongoing": { "type": "boolean" },
+ "ff_only_enabled": { "type": ["boolean", false] },
+ "should_be_rebased": { "type": "boolean" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json
index b7b2535c204..88a3cad62f6 100644
--- a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json
+++ b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json
@@ -5,11 +5,18 @@
{
"required" : [
"stats",
- "status"
+ "status",
+ "last_pipeline"
],
"properties": {
"stats": { "$ref": "../commit_stats.json" },
- "status": { "type": ["string", "null"] }
+ "status": { "type": ["string", "null"] },
+ "last_pipeline": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "../pipeline/basic.json" }
+ ]
+ }
}
}
]
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 8acd9488215..5c08dbc3b96 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -9,6 +9,8 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 31b3f4ba946..5828be5255b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -72,6 +72,7 @@
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"time_stats": {
"time_estimate": { "type": "integer" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
new file mode 100644
index 00000000000..0d127dc5297
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "sha",
+ "ref",
+ "status"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "sha": { "type": "string" },
+ "ref": { "type": "string" },
+ "status": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/admins.json b/spec/fixtures/api/schemas/public_api/v4/user/admins.json
new file mode 100644
index 00000000000..4a107f0ddbe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/admins.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "admin.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basics.json b/spec/fixtures/api/schemas/public_api/v4/user/basics.json
new file mode 100644
index 00000000000..6f7cf42229d
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basics.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "basic.json" }
+}
diff --git a/spec/fixtures/api/schemas/registry/repositories.json b/spec/fixtures/api/schemas/registry/repositories.json
new file mode 100644
index 00000000000..4978bd89cda
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/repositories.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "repository.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json
new file mode 100644
index 00000000000..4175642eb00
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/repository.json
@@ -0,0 +1,27 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "path",
+ "location",
+ "tags_path"
+ ],
+ "properties" : {
+ "id": {
+ "type": "integer"
+ },
+ "path": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "tags_path": {
+ "type": "string"
+ },
+ "destroy_path": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json
new file mode 100644
index 00000000000..5bc307e0e64
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/tag.json
@@ -0,0 +1,28 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "location"
+ ],
+ "properties" : {
+ "name": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "revision": {
+ "type": "string"
+ },
+ "total_size": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "destroy_path": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/registry/tags.json b/spec/fixtures/api/schemas/registry/tags.json
new file mode 100644
index 00000000000..c72f957459a
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/tags.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "tag.json"
+ }
+}
diff --git a/spec/fixtures/config/kubeconfig.yml b/spec/fixtures/config/kubeconfig.yml
index c4e8e573c32..5152dae0104 100644
--- a/spec/fixtures/config/kubeconfig.yml
+++ b/spec/fixtures/config/kubeconfig.yml
@@ -4,7 +4,7 @@ clusters:
- name: gitlab-deploy
cluster:
server: https://kube.domain.com
- certificate-authority-data: "UEVN\n"
+ certificate-authority-data: "UEVN"
contexts:
- name: gitlab-deploy
context:
diff --git a/spec/fixtures/trace/trace_with_sections b/spec/fixtures/trace/trace_with_sections
new file mode 100644
index 00000000000..21dff3928c3
--- /dev/null
+++ b/spec/fixtures/trace/trace_with_sections
@@ -0,0 +1,15 @@
+Running with gitlab-runner dev (HEAD)
+ on kitsune minikube (a21b584f)
+WARNING: Namespace is empty, therefore assuming 'default'.
+Using Kubernetes namespace: default
+Using Kubernetes executor with image alpine:3.4 ...
+section_start:1506004954:prepare_script Waiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending
+Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local...
+section_end:1506004957:prepare_script section_start:1506004957:get_sources Cloning repository...
+Cloning into '/nolith/ci-tests'...
+Checking out dddd7a6e as master...
+Skipping Git submodules setup
+section_end:1506004958:get_sources section_start:1506004958:restore_cache section_end:1506004958:restore_cache section_start:1506004958:download_artifacts section_end:1506004958:download_artifacts section_start:1506004958:build_script $ whoami
+root
+section_end:1506004959:build_script section_start:1506004959:after_script section_end:1506004959:after_script section_start:1506004959:archive_cache section_end:1506004959:archive_cache section_start:1506004959:upload_artifacts section_end:1506004959:upload_artifacts Job succeeded
+
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index b6d892548ef..5e272af6073 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -10,6 +10,8 @@ describe AutoDevopsHelper do
before do
allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed }
allow(helper).to receive(:current_user) { user }
+
+ Feature.get(:auto_devops_banner_disabled).disable
end
subject { helper.show_auto_devops_callout?(project) }
@@ -18,6 +20,14 @@ describe AutoDevopsHelper do
it { is_expected.to eq(true) }
end
+ context 'when the banner is disabled by feature flag' do
+ it 'allows the feature flag to disable' do
+ Feature.get(:auto_devops_banner_disabled).enable
+
+ expect(subject).to be(false)
+ end
+ end
+
context 'when dismissed' do
before do
helper.request.cookies[:auto_devops_settings_dismissed] = 'true'
@@ -55,5 +65,21 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
+
+ context 'when master contains a .gitlab-ci.yml file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when another service is enabled' do
+ before do
+ create(:service, project: project, category: :ci, active: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 4632c679972..f44e7ef6843 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -26,12 +26,13 @@ describe AvatarsHelper do
subject { helper.user_avatar_without_link(options) }
it 'displays user avatar' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 has-tooltip lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: avatar_icon(user, 16) }
+ src: avatar_icon(user, 16),
+ data: { container: 'body' },
+ class: 'avatar s16 has-tooltip',
+ title: user.name
)
end
@@ -39,12 +40,13 @@ describe AvatarsHelper do
let(:options) { { user: user, css_class: '.cat-pics' } }
it 'uses provided css_class' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: "avatar s16 #{options[:css_class]} has-tooltip lazy",
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: avatar_icon(user, 16) }
+ src: avatar_icon(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 #{options[:css_class]} has-tooltip",
+ title: user.name
)
end
end
@@ -53,12 +55,13 @@ describe AvatarsHelper do
let(:options) { { user: user, size: 99 } }
it 'uses provided size' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: "avatar s#{options[:size]} has-tooltip lazy",
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: avatar_icon(user, options[:size]) }
+ src: avatar_icon(user, options[:size]),
+ data: { container: 'body' },
+ class: "avatar s#{options[:size]} has-tooltip",
+ title: user.name
)
end
end
@@ -67,12 +70,28 @@ describe AvatarsHelper do
let(:options) { { user: user, url: '/over/the/rainbow.png' } }
it 'uses provided url' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 has-tooltip lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: options[:url] }
+ src: options[:url],
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name
+ )
+ end
+ end
+
+ context 'with lazy parameter' do
+ let(:options) { { user: user, lazy: true } }
+
+ it 'adds `lazy` class to class list, sets `data-src` with avatar URL and `src` with placeholder image' do
+ is_expected.to eq tag(
+ :img,
+ alt: "#{user.name}'s avatar",
+ src: LazyImageTagHelper.placeholder_image,
+ data: { container: 'body', src: avatar_icon(user, 16) },
+ class: "avatar s16 has-tooltip lazy",
+ title: user.name
)
end
end
@@ -82,12 +101,13 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: true } }
it 'adds has-tooltip' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 has-tooltip lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: avatar_icon(user, 16) }
+ src: avatar_icon(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name
)
end
end
@@ -96,12 +116,12 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: false } }
it 'does not add has-tooltip or data container' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { src: avatar_icon(user, 16) }
+ src: avatar_icon(user, 16),
+ class: "avatar s16",
+ title: user.name
)
end
end
@@ -114,23 +134,25 @@ describe AvatarsHelper do
let(:options) { { user: user, user_name: 'Tinky Winky' } }
it 'prefers user parameter' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 has-tooltip lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body', src: avatar_icon(user, 16) }
+ src: avatar_icon(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name
)
end
end
it 'uses user_name and user_email parameter if user is not present' do
- is_expected.to eq image_tag(
- LazyImageTagHelper.placeholder_image,
- class: 'avatar s16 has-tooltip lazy',
+ is_expected.to eq tag(
+ :img,
alt: "#{options[:user_name]}'s avatar",
- title: options[:user_name],
- data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
+ src: avatar_icon(options[:user_email], 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: options[:user_name]
)
end
end
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 7179185285c..4b6c7c33e5b 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -12,6 +12,17 @@ describe CommitsHelper do
expect(helper.commit_author_link(commit))
.not_to include('onmouseover="alert(1)"')
end
+
+ it 'escapes the author name' do
+ user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>')
+
+ commit = double(author: user, author_name: '', author_email: '')
+
+ expect(helper.commit_author_link(commit))
+ .to include('Foo &lt;script&gt;')
+ expect(helper.commit_author_link(commit, avatar: true))
+ .to include('commit-author-name', 'Foo &lt;script&gt;')
+ end
end
describe 'commit_committer_link' do
@@ -25,6 +36,17 @@ describe CommitsHelper do
expect(helper.commit_committer_link(commit))
.not_to include('onmouseover="alert(1)"')
end
+
+ it 'escapes the commiter name' do
+ user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>')
+
+ commit = double(committer: user, committer_name: '', committer_email: '')
+
+ expect(helper.commit_committer_link(commit))
+ .to include('Foo &lt;script&gt;')
+ expect(helper.commit_committer_link(commit, avatar: true))
+ .to include('commit-committer-name', 'Foo &lt;script&gt;')
+ end
end
describe '#view_on_environment_button' do
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 0deea0ff6a3..f9c31ac61d8 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -136,9 +136,9 @@ describe DiffHelper do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>})
- expect(marked_old_line).not_to be_html_safe
+ expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
- expect(marked_new_line).not_to be_html_safe
+ expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 36031ac1a28..76e5964ccf7 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -17,7 +17,7 @@ describe GroupsHelper do
it 'gives default avatar_icon when no avatar is present' do
group = create(:group)
group.save!
- expect(group_icon(group.path)).to match('group_avatar.png')
+ expect(group_icon(group.path)).to match_asset_path('group_avatar.png')
end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 91c8faea7fd..3d79dac284f 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -16,6 +16,25 @@ describe IconsHelper do
end
end
+ describe 'sprite_icon' do
+ icon_name = 'clock'
+
+ it 'returns svg icon html' do
+ expect(sprite_icon(icon_name).to_s)
+ .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ end
+
+ it 'returns svg icon html + size classes' do
+ expect(sprite_icon(icon_name, size: 72).to_s)
+ .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ end
+
+ it 'returns svg icon html + size classes + additional class' do
+ expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
+ .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ 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/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 7d1c17909bf..fd7900c32f4 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe MergeRequestsHelper do
+ include ProjectForksHelper
describe 'ci_build_details_path' do
let(:project) { create(:project) }
let(:merge_request) { MergeRequest.new }
@@ -31,10 +32,10 @@ describe MergeRequestsHelper do
describe 'within different projects' do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
- let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
+ let(:forked_project) { fork_project(project) }
+ let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) }
subject { format_mr_branch_names(merge_request) }
- let(:source_title) { "#{fork_project.full_path}:#{merge_request.source_branch}" }
+ let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" }
let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" }
it { is_expected.to eq([source_title, target_title]) }
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 9aca3987657..baf927a9acc 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -54,7 +54,7 @@ describe PageLayoutHelper do
describe 'page_image' do
it 'defaults to the GitLab logo' do
- expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
%w(project user group).each do |type|
@@ -70,13 +70,13 @@ describe PageLayoutHelper do
object = double(avatar_url: nil)
assign(type, object)
- expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
end
context "with no assignments" do
it 'falls back to the default' do
- expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 49cb7c954b4..5777b5c4025 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,7 +63,7 @@ describe ProjectsHelper do
end
end
- describe "#project_list_cache_key", clean_gitlab_redis_shared_state: true do
+ describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do
let(:project) { create(:project, :repository) }
it "includes the route" do
@@ -200,13 +200,13 @@ describe ProjectsHelper do
end
it 'returns image tag for member avatar' do
- expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16"], alt: "" })
+ expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16"], alt: "", "data-src" => anything })
helper.link_to_member_avatar(user)
end
it 'returns image tag with avatar class' do
- expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16", "any-avatar-class"], alt: "" })
+ expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16", "any-avatar-class"], alt: "", "data-src" => anything })
helper.link_to_member_avatar(user, avatar_class: "any-avatar-class")
end
@@ -313,23 +313,10 @@ describe ProjectsHelper do
it 'returns recent push on the current project' do
event = double(:event)
- expect(user).to receive(:recent_push).with([project.id]).and_return(event)
+ expect(user).to receive(:recent_push).with(project).and_return(event)
expect(helper.last_push_event).to eq(event)
end
-
- context 'when current user has a fork of the current project' do
- let(:fork) { double(:fork, id: 2) }
-
- it 'returns recent push considering fork events' do
- expect(user).to receive(:fork_of).with(project).and_return(fork)
-
- event_on_fork = double(:event)
- expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork)
-
- expect(helper.last_push_event).to eq(event_on_fork)
- end
- end
end
describe "#project_feature_access_select" do
@@ -433,22 +420,26 @@ describe ProjectsHelper do
end
end
- describe '#has_projects_or_name?' do
+ describe '#show_projects' do
let(:projects) do
create(:project)
Project.all
end
it 'returns true when there are projects' do
- expect(helper.has_projects_or_name?(projects, {})).to eq(true)
+ expect(helper.show_projects?(projects, {})).to eq(true)
end
it 'returns true when there are no projects but a name is given' do
- expect(helper.has_projects_or_name?(Project.none, name: 'foo')).to eq(true)
+ expect(helper.show_projects?(Project.none, name: 'foo')).to eq(true)
+ end
+
+ it 'returns true when there are no projects but personal is present' do
+ expect(helper.show_projects?(Project.none, personal: 'true')).to eq(true)
end
it 'returns false when there are no projects and there is no name' do
- expect(helper.has_projects_or_name?(Project.none, {})).to eq(false)
+ expect(helper.show_projects?(Project.none, {})).to eq(false)
end
end
@@ -482,4 +473,15 @@ describe ProjectsHelper do
expect(recorder.count).to eq(1)
end
end
+
+ describe '#git_user_name' do
+ let(:user) { double(:user, name: 'John "A" Doe53') }
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'parses quotes in name' do
+ expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53')
+ end
+ end
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index c4f4e0d21dc..5a2e4b34069 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -147,6 +147,12 @@ describe SubmoduleHelper do
expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
end
+ it 'sanitizes invalid URL with extended ASCII' do
+ stub_url('é')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 74bdbb01166..1a78196e33d 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -9,8 +9,8 @@ describe Doorkeeper.configuration do
end
describe '#optional_scopes' do
- it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
- expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
+ it 'matches Gitlab::Auth.optional_scopes' do
+ expect(subject.optional_scopes).to eq Gitlab::Auth.optional_scopes - Gitlab::Auth::REGISTRY_SCOPES
end
end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 84ad55e9f98..d56e14e0e0b 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -36,10 +36,10 @@ describe 'create_tokens' do
expect(keys).to all(match(HEX_KEY))
end
- it 'generates an RSA key for jws_private_key' do
+ it 'generates an RSA key for openid_connect_signing_key' do
create_tokens
- keys = secrets.values_at(:jws_private_key)
+ keys = secrets.values_at(:openid_connect_signing_key)
expect(keys.uniq).to eq(keys)
expect(keys).to all(match(RSA_KEY))
@@ -49,7 +49,7 @@ describe 'create_tokens' do
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_key_base')
- expect(self).to receive(:warn_missing_secret).with('jws_private_key')
+ expect(self).to receive(:warn_missing_secret).with('openid_connect_signing_key')
create_tokens
end
@@ -61,7 +61,7 @@ describe 'create_tokens' do
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
- expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
+ expect(new_secrets['openid_connect_signing_key']).to eq(secrets.openid_connect_signing_key)
end
create_tokens
@@ -77,7 +77,7 @@ describe 'create_tokens' do
context 'when the other secrets all exist' do
before do
secrets.db_key_base = 'db_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
allow(File).to receive(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key')
@@ -88,7 +88,7 @@ describe 'create_tokens' do
stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
end
it 'does not issue a warning' do
@@ -114,7 +114,7 @@ describe 'create_tokens' do
before do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
end
it 'does not write any files' do
@@ -129,7 +129,7 @@ describe 'create_tokens' do
expect(secrets.secret_key_base).to eq('secret_key_base')
expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
- expect(secrets.jws_private_key).to eq('jws_private_key')
+ expect(secrets.openid_connect_signing_key).to eq('openid_connect_signing_key')
end
it 'deletes the .secret file' do
@@ -153,7 +153,7 @@ describe 'create_tokens' do
expect(new_secrets['secret_key_base']).to eq('file_key')
expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
- expect(new_secrets['jws_private_key']).to eq('jws_private_key')
+ expect(new_secrets['openid_connect_signing_key']).to eq('openid_connect_signing_key')
end
create_tokens
diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js
index 13cab81dd60..7f6b5873011 100644
--- a/spec/javascripts/abuse_reports_spec.js
+++ b/spec/javascripts/abuse_reports_spec.js
@@ -1,43 +1,41 @@
import '~/lib/utils/text_utility';
-import '~/abuse_reports';
-
-((global) => {
- describe('Abuse Reports', () => {
- const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
- const MAX_MESSAGE_LENGTH = 500;
-
- let $messages;
-
- const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- const findMessage = searchText => $messages.filter(
- (index, element) => element.innerText.indexOf(searchText) > -1,
- ).first();
-
- preloadFixtures(FIXTURE);
-
- beforeEach(function () {
- loadFixtures(FIXTURE);
- this.abuseReports = new global.AbuseReports();
- $messages = $('.abuse-reports .message');
- });
-
- it('should truncate long messages', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('original-message')).toEqual(jasmine.anything());
- assertMaxLength($longMessage);
- });
-
- it('should not truncate short messages', () => {
- const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
- });
-
- it('should allow clicking a truncated message to expand and collapse the full message', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- $longMessage.click();
- expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
- $longMessage.click();
- assertMaxLength($longMessage);
- });
+import AbuseReports from '~/abuse_reports';
+
+describe('Abuse Reports', () => {
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ let $messages;
+
+ const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ const findMessage = searchText => $messages.filter(
+ (index, element) => element.innerText.indexOf(searchText) > -1,
+ ).first();
+
+ preloadFixtures(FIXTURE);
+
+ beforeEach(function () {
+ loadFixtures(FIXTURE);
+ this.abuseReports = new AbuseReports();
+ $messages = $('.abuse-reports .message');
+ });
+
+ it('should truncate long messages', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', () => {
+ const $shortMessage = findMessage('SHORT MESSAGE');
+ expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+ });
+
+ it('should allow clicking a truncated message to expand and collapse the full message', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
});
-})(window.gl);
+});
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
index 46e072a8ebb..c93b7cc6cac 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -1,6 +1,6 @@
import 'jquery';
import 'jquery-ujs';
-import '~/ajax_loading_spinner';
+import AjaxLoadingSpinner from '~/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
@@ -8,7 +8,7 @@ describe('Ajax Loading Spinner', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- gl.AjaxLoadingSpinner.init();
+ AjaxLoadingSpinner.init();
});
it('change current icon with spinner icon and disable link while waiting ajax response', (done) => {
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
index 11f2a950678..c3e67550f05 100644
--- a/spec/javascripts/blob/notebook/index_spec.js
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => {
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
- ).toBe('An error occured whilst parsing the file.');
+ ).toBe('An error occurred whilst parsing the file.');
});
});
@@ -153,7 +153,7 @@ describe('iPython notebook renderer', () => {
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
- ).toBe('An error occured whilst loading the file. Please try again later.');
+ ).toBe('An error occurred whilst loading the file. Please try again later.');
});
});
});
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index bbeaf95e68d..51bf3086627 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -76,7 +76,7 @@ describe('PDF renderer', () => {
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
- ).toBe('An error occured whilst loading the file. Please try again later.');
+ ).toBe('An error occurred whilst loading the file. Please try again later.');
});
});
});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 35149611095..d5b0f23e7b7 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -289,4 +289,18 @@ describe('Build', () => {
});
});
});
+
+ describe('getBuildTrace', () => {
+ it('should request build trace with state parameter', (done) => {
+ spyOn(jQuery, 'ajax').and.callThrough();
+ new Build();
+
+ setTimeout(() => {
+ expect(jQuery.ajax).toHaveBeenCalledWith(
+ { url: `${BUILD_URL}/trace.json`, data: { state: '' } },
+ );
+ done();
+ }, 0);
+ });
+ });
});
diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js
new file mode 100644
index 00000000000..eb1cd6eb804
--- /dev/null
+++ b/spec/javascripts/clusters_spec.js
@@ -0,0 +1,79 @@
+import Clusters from '~/clusters';
+
+describe('Clusters', () => {
+ let cluster;
+ preloadFixtures('clusters/show_cluster.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('clusters/show_cluster.html.raw');
+ cluster = new Clusters();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', () => {
+ cluster.toggleButton.click();
+
+ expect(
+ cluster.toggleButton.classList,
+ ).not.toContain('checked');
+
+ expect(
+ cluster.toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer('creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container', () => {
+ cluster.updateContainer('created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer('errored', 'this is an error');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+
+ expect(
+ cluster.errorReasonContainer.textContent,
+ ).toContain('this is an error');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 454f187ccbc..9fc047b1f5e 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -29,6 +29,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -65,6 +67,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -117,6 +121,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -139,6 +145,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index ace95000468..e5a5e3293b9 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -1,77 +1,73 @@
-/* global CommitsList */
-
import 'vendor/jquery.endless-scroll';
import '~/pager';
-import '~/commits';
-
-(() => {
- describe('Commits List', () => {
- beforeEach(() => {
- setFixtures(`
- <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
- <input id="commits-search">
- </form>
- <ol id="commits-list"></ol>
- `);
- });
+import CommitsList from '~/commits';
- it('should be defined', () => {
- expect(CommitsList).toBeDefined();
- });
+describe('Commits List', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ });
- describe('processCommits', () => {
- it('should join commit headers', () => {
- CommitsList.$contentList = $(`
- <div>
- <li class="commit-header" data-day="2016-09-20">
- <span class="day">20 Sep, 2016</span>
- <span class="commits-count">1 commit</span>
- </li>
- <li class="commit"></li>
- </div>
- `);
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
- const data = `
+ describe('processCommits', () => {
+ it('should join commit headers', () => {
+ CommitsList.$contentList = $(`
+ <div>
<li class="commit-header" data-day="2016-09-20">
<span class="day">20 Sep, 2016</span>
<span class="commits-count">1 commit</span>
</li>
<li class="commit"></li>
- `;
+ </div>
+ `);
- // The last commit header should be removed
- // since the previous one has the same data-day value.
- expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
- });
+ const data = `
+ <li class="commit-header" data-day="2016-09-20">
+ <span class="day">20 Sep, 2016</span>
+ <span class="commits-count">1 commit</span>
+ </li>
+ <li class="commit"></li>
+ `;
+
+ // The last commit header should be removed
+ // since the previous one has the same data-day value.
+ expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
});
+ });
- describe('on entering input', () => {
- let ajaxSpy;
+ describe('on entering input', () => {
+ let ajaxSpy;
- beforeEach(() => {
- CommitsList.init(25);
- CommitsList.searchField.val('');
+ beforeEach(() => {
+ CommitsList.init(25);
+ CommitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
- ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
- req.success({
- data: '<li>Result</li>',
- });
+ spyOn(history, 'replaceState').and.stub();
+ ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
+ req.success({
+ data: '<li>Result</li>',
});
});
+ });
- it('should save the last search string', () => {
- CommitsList.searchField.val('GitLab');
- CommitsList.filterResults();
- expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
- });
+ it('should save the last search string', () => {
+ CommitsList.searchField.val('GitLab');
+ CommitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+ });
- it('should not make ajax call if the input does not change', () => {
- CommitsList.filterResults();
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
- });
+ it('should not make ajax call if the input does not change', () => {
+ CommitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
});
});
-})();
+});
diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js
new file mode 100644
index 00000000000..fb6b7fee168
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/banner_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import banner from '~/cycle_analytics/components/banner.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Cycle analytics banner', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(banner);
+ vm = mountComponent(Component, {
+ documentationLink: 'path',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render cycle analytics information', () => {
+ expect(
+ vm.$el.querySelector('h4').textContent.trim(),
+ ).toEqual('Introducing Cycle Analytics');
+ expect(
+ vm.$el.querySelector('p').textContent.trim(),
+ ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.');
+ expect(
+ vm.$el.querySelector('a').textContent.trim(),
+ ).toEqual('Read more');
+ expect(
+ vm.$el.querySelector('a').getAttribute('href'),
+ ).toEqual('path');
+ });
+
+ it('should emit an event when close button is clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-ca-dismiss-button').click();
+
+ expect(vm.$emit).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
index 2fb9eb0ca85..13e9fe00a00 100644
--- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
+++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import limitWarningComp from '~/cycle_analytics/components/limit_warning_component';
+import limitWarningComp from '~/cycle_analytics/components/limit_warning_component.vue';
Vue.use(Translate);
diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js
new file mode 100644
index 00000000000..31b65fd1cde
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import component from '~/cycle_analytics/components/total_time_component.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Total time component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('With data', () => {
+ it('should render information for days and hours', () => {
+ vm = mountComponent(Component, {
+ time: {
+ days: 3,
+ hours: 4,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs');
+ });
+
+ it('should render information for hours and minutes', () => {
+ vm = mountComponent(Component, {
+ time: {
+ hours: 4,
+ mins: 35,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins');
+ });
+
+ it('should render information for seconds', () => {
+ vm = mountComponent(Component, {
+ time: {
+ seconds: 45,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('45 s');
+ });
+ });
+
+ describe('Without data', () => {
+ it('should render no information', () => {
+ vm = mountComponent(Component);
+
+ expect(vm.$el.textContent.trim()).toEqual('--');
+ });
+ });
+});
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index fdaea5c0b0c..7e62d356bd2 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -14,6 +14,10 @@ describe('Environments Folder View', () => {
window.history.pushState({}, null, 'environments/folders/build');
});
+ afterEach(() => {
+ window.history.pushState({}, null, '/');
+ });
+
let component;
describe('successfull request', () => {
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
deleted file mode 100644
index 114d282e48a..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ /dev/null
@@ -1,219 +0,0 @@
-import Cookies from 'js-cookie';
-import {
- getCookieName,
- getSelector,
- showPopover,
- hidePopover,
- dismiss,
- mouseleave,
- mouseenter,
- setupDismissButton,
-} from '~/feature_highlight/feature_highlight_helper';
-
-describe('feature highlight helper', () => {
- describe('getCookieName', () => {
- it('returns `feature-highlighted-` prefix', () => {
- const cookieId = 'cookieId';
- expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`);
- });
- });
-
- describe('getSelector', () => {
- it('returns js-feature-highlight selector', () => {
- const highlightId = 'highlightId';
- expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
- });
- });
-
- describe('showPopover', () => {
- it('returns true when popover is shown', () => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- expect(showPopover.call(context)).toEqual(true);
- });
-
- it('returns false when popover is already shown', () => {
- const context = {
- hasClass: () => true,
- };
-
- expect(showPopover.call(context)).toEqual(false);
- });
-
- it('shows popover', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('show');
- done();
- });
-
- showPopover.call(context);
- });
-
- it('adds disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- spyOn(context, 'addClass').and.callFake((classNames) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- done();
- });
-
- showPopover.call(context);
- });
- });
-
- describe('hidePopover', () => {
- it('returns true when popover is hidden', () => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- expect(hidePopover.call(context)).toEqual(true);
- });
-
- it('returns false when popover is already hidden', () => {
- const context = {
- hasClass: () => false,
- };
-
- expect(hidePopover.call(context)).toEqual(false);
- });
-
- it('hides popover', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('hide');
- done();
- });
-
- hidePopover.call(context);
- });
-
- it('removes disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- spyOn(context, 'removeClass').and.callFake((classNames) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- done();
- });
-
- hidePopover.call(context);
- });
- });
-
- describe('dismiss', () => {
- const context = {
- hide: () => {},
- };
-
- beforeEach(() => {
- spyOn(Cookies, 'set').and.callFake(() => {});
- spyOn(hidePopover, 'call').and.callFake(() => {});
- spyOn(context, 'hide').and.callFake(() => {});
- dismiss.call(context);
- });
-
- it('sets cookie to true', () => {
- expect(Cookies.set).toHaveBeenCalled();
- });
-
- it('calls hide popover', () => {
- expect(hidePopover.call).toHaveBeenCalled();
- });
-
- it('calls hide', () => {
- expect(context.hide).toHaveBeenCalled();
- });
- });
-
- describe('mouseleave', () => {
- it('calls hide popover if .popover:hover is false', () => {
- const fakeJquery = {
- length: 0,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(hidePopover, 'call');
- mouseleave();
- expect(hidePopover.call).toHaveBeenCalled();
- });
-
- it('does not call hide popover if .popover:hover is true', () => {
- const fakeJquery = {
- length: 1,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(hidePopover, 'call');
- mouseleave();
- expect(hidePopover.call).not.toHaveBeenCalled();
- });
- });
-
- describe('mouseenter', () => {
- const context = {};
-
- it('shows popover', () => {
- spyOn(showPopover, 'call').and.returnValue(false);
- mouseenter.call(context);
- expect(showPopover.call).toHaveBeenCalled();
- });
-
- it('registers mouseleave event if popover is showed', (done) => {
- spyOn(showPopover, 'call').and.returnValue(true);
- spyOn($.fn, 'on').and.callFake((eventName) => {
- expect(eventName).toEqual('mouseleave');
- done();
- });
- mouseenter.call(context);
- });
-
- it('does not register mouseleave event if popover is not showed', () => {
- spyOn(showPopover, 'call').and.returnValue(false);
- const spy = spyOn($.fn, 'on').and.callFake(() => {});
- mouseenter.call(context);
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
- describe('setupDismissButton', () => {
- it('registers click event callback', (done) => {
- const context = {
- getAttribute: () => 'popoverId',
- dataset: {
- highlight: 'cookieId',
- },
- };
-
- spyOn($.fn, 'on').and.callFake((event) => {
- expect(event).toEqual('click');
- done();
- });
- setupDismissButton.call(context);
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
deleted file mode 100644
index 7feb361edec..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import domContentLoaded from '~/feature_highlight/feature_highlight_options';
-import bp from '~/breakpoints';
-
-describe('feature highlight options', () => {
- describe('domContentLoaded', () => {
- const highlightOrder = [];
-
- beforeEach(() => {
- // Check for when highlightFeatures is called
- spyOn(highlightOrder, 'find').and.callFake(() => {});
- });
-
- it('should not call highlightFeatures when breakpoint is xs', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should not call highlightFeatures when breakpoint is sm', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should not call highlightFeatures when breakpoint is md', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should call highlightFeatures when breakpoint is lg', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
deleted file mode 100644
index 6abe8425ee7..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import Cookies from 'js-cookie';
-import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
-import * as featureHighlight from '~/feature_highlight/feature_highlight';
-
-describe('feature highlight', () => {
- describe('setupFeatureHighlightPopover', () => {
- const selector = '.js-feature-highlight[data-highlight=test]';
- beforeEach(() => {
- setFixtures(`
- <div>
- <div class="js-feature-highlight" data-highlight="test" disabled>
- Trigger
- </div>
- </div>
- <div class="feature-highlight-popover-content">
- Content
- <div class="dismiss-feature-highlight">
- Dismiss
- </div>
- </div>
- `);
- spyOn(window, 'addEventListener');
- spyOn(window, 'removeEventListener');
- featureHighlight.setupFeatureHighlightPopover('test', 0);
- });
-
- it('setups popover content', () => {
- const $popoverContent = $('.feature-highlight-popover-content');
- const outerHTML = $popoverContent.prop('outerHTML');
-
- expect($(selector).data('content')).toEqual(outerHTML);
- });
-
- it('setups mouseenter', () => {
- const showSpy = spyOn(featureHighlightHelper.showPopover, 'call');
- $(selector).trigger('mouseenter');
-
- expect(showSpy).toHaveBeenCalled();
- });
-
- it('setups debounced mouseleave', (done) => {
- const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call');
- $(selector).trigger('mouseleave');
-
- // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
- setTimeout(() => {
- expect(hideSpy).toHaveBeenCalled();
- done();
- }, 0);
- });
-
- it('setups inserted.bs.popover', () => {
- $(selector).trigger('mouseenter');
- const popoverId = $(selector).attr('aria-describedby');
- const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
-
- $(`#${popoverId} .dismiss-feature-highlight`).click();
- expect(spyEvent).toHaveBeenTriggered();
- });
-
- it('setups show.bs.popover', () => {
- $(selector).trigger('show.bs.popover');
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
- });
-
- it('setups hide.bs.popover', () => {
- $(selector).trigger('hide.bs.popover');
- expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
- });
-
- it('removes disabled attribute', () => {
- expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
- });
-
- it('displays popover', () => {
- expect($(selector).attr('aria-describedby')).toBeFalsy();
- $(selector).trigger('mouseenter');
- expect($(selector).attr('aria-describedby')).toBeTruthy();
- });
- });
-
- describe('shouldHighlightFeature', () => {
- it('should return false if element is not found', () => {
- spyOn(document, 'querySelector').and.returnValue(null);
- spyOn(Cookies, 'get').and.returnValue(null);
-
- expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
- });
-
- it('should return false if previouslyDismissed', () => {
- spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
- spyOn(Cookies, 'get').and.returnValue('true');
-
- expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
- });
-
- it('should return true if element is found and not previouslyDismissed', () => {
- spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
- spyOn(Cookies, 'get').and.returnValue(null);
-
- expect(featureHighlight.shouldHighlightFeature()).toBeTruthy();
- });
- });
-
- describe('highlightFeatures', () => {
- it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => {
- // Mimic shouldHighlightFeature set to true
- const highlightOrder = ['issue-boards'];
- spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]);
-
- expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true);
- });
-
- it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => {
- // Mimic shouldHighlightFeature set to false
- const highlightOrder = ['issue-boards'];
- spyOn(highlightOrder, 'find').and.returnValue(null);
-
- expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false);
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index b3c9bca64cc..02415485d19 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -10,6 +10,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new gl.DropdownUser({
@@ -38,6 +39,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
});
it('should return endpoint', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 16ae649ee60..f209328dee1 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -411,4 +411,26 @@ describe('Filtered Search Manager', () => {
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
});
});
+
+ describe('getAllParams', () => {
+ beforeEach(() => {
+ this.paramsArr = ['key=value', 'otherkey=othervalue'];
+
+ initializeManager();
+ });
+
+ it('correctly modifies params when custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call({
+ modifyUrlParams: paramsArr => paramsArr.reverse(),
+ }, [].concat(this.paramsArr));
+
+ expect(modifedParams[0]).toBe(this.paramsArr[1]);
+ });
+
+ it('does not modify params when no custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call({}, this.paramsArr);
+
+ expect(modifedParams[1]).toBe(this.paramsArr[1]);
+ });
+ });
});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
new file mode 100644
index 00000000000..5774f36f026
--- /dev/null
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace) }
+ let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('clusters/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'clusters/show_cluster.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: cluster
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb
deleted file mode 100644
index 7fa351680c9..00000000000
--- a/spec/javascripts/fixtures/dashboard.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
-
- let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
-
- render_views
-
- before(:all) do
- clean_frontend_fixtures('dashboard/')
- end
-
- before do
- sign_in(admin)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'dashboard/user-callout.html.raw' do |example|
- rendered = render_template('shared/_user_callout')
- store_frontend_fixture(rendered, example.description)
- end
-
- private
-
- def render_template(template_file_name)
- controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH)
- controller.render_to_string(template_file_name, layout: false)
- end
-end
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 4bc2205e642..3fd16d76f51 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
remove_repository(project)
end
+ it 'merge_requests/merge_request_of_current_user.html.raw' do |example|
+ merge_request.update(author: admin)
+
+ render_merge_request(example.description, merge_request)
+ end
+
it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
create(:ci_build, :pending, pipeline: pipeline)
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
index 418a38a0e2e..97b0c25c923 100644
--- a/spec/javascripts/fixtures/pipelines.html.haml
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -2,6 +2,8 @@
#pipelines-list-vue{ data: { endpoint: 'foo',
"css-class" => 'foo',
"help-page-path" => 'foo',
+ "empty-state-svg-path" => 'foo',
+ "error-state-svg-path" => 'foo',
"new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true',
"all-path" => 'foo',
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index f8b37c0edde..4f20e31f511 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => {
).toBe(0);
});
+ it('returns 0 if mousePos is empty', () => {
+ expect(
+ getHideSubItemsInterval(),
+ ).toBe(0);
+ });
+
it('returns 0 when mouse above sub-items', () => {
showSubLevelItems(el);
documentMouseMove({
@@ -271,12 +277,19 @@ describe('Fly out sidebar navigation', () => {
});
it('sets transform of sub-items', () => {
+ const sidebar = document.createElement('div');
const subItems = el.querySelector('.sidebar-sub-level-items');
+
+ sidebar.style.width = '200px';
+
+ document.body.appendChild(sidebar);
+
+ setSidebar(sidebar);
showSubLevelItems(el);
expect(
subItems.style.transform,
- ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
+ ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
});
it('sets is-above when element is above', () => {
diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js
index 2d386fe1da5..2d386fe1da5 100644
--- a/spec/javascripts/notes/stores/helpers.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
new file mode 100644
index 00000000000..fb9c7e59031
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
@@ -0,0 +1,132 @@
+import * as badgeHelper from '~/image_diff/helpers/badge_helper';
+import * as mockData from '../mock_data';
+
+describe('badge helper', () => {
+ const { coordinate, noteId, badgeText, badgeNumber } = mockData;
+ let containerEl;
+ let buttonEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('createImageBadge', () => {
+ beforeEach(() => {
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate);
+ });
+
+ it('should create button', () => {
+ expect(buttonEl.tagName).toEqual('BUTTON');
+ expect(buttonEl.getAttribute('type')).toEqual('button');
+ });
+
+ it('should set disabled attribute', () => {
+ expect(buttonEl.hasAttribute('disabled')).toEqual(true);
+ });
+
+ it('should set noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ describe('classNames', () => {
+ it('should set .js-image-badge by default', () => {
+ expect(buttonEl.className).toEqual('js-image-badge');
+ });
+
+ it('should add additional class names if parameter is passed', () => {
+ const classNames = ['first-class', 'second-class'];
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames);
+
+ expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' '));
+ });
+ });
+ });
+
+ describe('addImageBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageBadge(containerEl, {
+ coordinate,
+ badgeText,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should appends button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should set the badge text', () => {
+ expect(buttonEl.innerText).toEqual(badgeText);
+ });
+
+ it('should set the button coordinates', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should set the button noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+ });
+
+ describe('addImageCommentBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageCommentBadge(containerEl, {
+ coordinate,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append icon button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should create icon comment button', () => {
+ const iconEl = buttonEl.querySelector('i');
+ expect(iconEl).toBeDefined();
+ expect(iconEl.classList.contains('fa')).toEqual(true);
+ expect(iconEl.classList.contains('fa-comment-o')).toEqual(true);
+ });
+
+ it('should have .image-comment-badge.inverted in button class', () => {
+ expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true);
+ expect(buttonEl.classList.contains('inverted')).toEqual(true);
+ });
+ });
+
+ describe('addAvatarBadge', () => {
+ let avatarBadgeEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div id="${noteId}">
+ <div class="badge hidden">
+ </div>
+ </div>
+ `;
+
+ badgeHelper.addAvatarBadge(containerEl, {
+ detail: {
+ noteId,
+ badgeNumber,
+ },
+ });
+ avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
+ });
+
+ it('should update badge number', () => {
+ expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString());
+ });
+
+ it('should remove hidden class', () => {
+ expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
new file mode 100644
index 00000000000..a284b981d2a
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
@@ -0,0 +1,139 @@
+import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
+import * as mockData from '../mock_data';
+
+describe('commentIndicatorHelper', () => {
+ const { coordinate } = mockData;
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('addCommentIndicator', () => {
+ let buttonEl;
+
+ beforeEach(() => {
+ commentIndicatorHelper.addCommentIndicator(containerEl, coordinate);
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ describe('button', () => {
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should contain image-comment-dark svg', () => {
+ const svgEl = buttonEl.querySelector('svg');
+ expect(svgEl).toBeDefined();
+
+ const svgLink = svgEl.querySelector('use').getAttribute('xlink:href');
+ expect(svgLink.indexOf('image-comment-dark') !== -1).toEqual(true);
+ });
+ });
+ });
+
+ describe('removeCommentIndicator', () => {
+ it('should return removed false if there is no comment-indicator', () => {
+ const result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ expect(result.removed).toEqual(false);
+ });
+
+ describe('has comment indicator', () => {
+ let result;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ `;
+ result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ });
+
+ it('should remove comment indicator', () => {
+ expect(containerEl.querySelector('.comment-indicator')).toBeNull();
+ });
+
+ it('should return removed true', () => {
+ expect(result.removed).toEqual(true);
+ });
+
+ it('should return indicator meta', () => {
+ expect(result.x).toEqual(coordinate.x);
+ expect(result.y).toEqual(coordinate.y);
+ expect(result.image).toBeDefined();
+ expect(result.image.width).toBeDefined();
+ expect(result.image.height).toBeDefined();
+ });
+ });
+ });
+
+ describe('showCommentIndicator', () => {
+ describe('commentIndicator exists', () => {
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <button class="comment-indicator"></button>
+ `;
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should set commentIndicator coordinates', () => {
+ const commentIndicatorEl = containerEl.querySelector('.comment-indicator');
+ expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+
+ describe('commentIndicator does not exist', () => {
+ beforeEach(() => {
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should addCommentIndicator', () => {
+ const buttonEl = containerEl.querySelector('.comment-indicator');
+ expect(buttonEl).toBeDefined();
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+ });
+
+ describe('commentIndicatorOnClick', () => {
+ let event;
+ let textAreaEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="diff-viewer">
+ <button></button>
+ <div class="note-container">
+ <textarea class="note-textarea"></textarea>
+ </div>
+ </div>
+ `;
+ textAreaEl = containerEl.querySelector('textarea');
+
+ event = {
+ stopPropagation: () => {},
+ currentTarget: containerEl.querySelector('button'),
+ };
+
+ spyOn(event, 'stopPropagation');
+ spyOn(textAreaEl, 'focus');
+ commentIndicatorHelper.commentIndicatorOnClick(event);
+ });
+
+ it('should stopPropagation', () => {
+ expect(event.stopPropagation).toHaveBeenCalled();
+ });
+
+ it('should focus textAreaEl', () => {
+ expect(textAreaEl.focus).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js
new file mode 100644
index 00000000000..8dde924e8ae
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/dom_helper_spec.js
@@ -0,0 +1,118 @@
+import * as domHelper from '~/image_diff/helpers/dom_helper';
+import * as mockData from '../mock_data';
+
+describe('domHelper', () => {
+ const { imageMeta, badgeNumber } = mockData;
+
+ describe('setPositionDataAttribute', () => {
+ let containerEl;
+ let attributeAfterCall;
+ const position = {
+ myProperty: 'myProperty',
+ };
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ containerEl.dataset.position = JSON.stringify(position);
+ domHelper.setPositionDataAttribute(containerEl, imageMeta);
+ attributeAfterCall = JSON.parse(containerEl.dataset.position);
+ });
+
+ it('should set x, y, width, height', () => {
+ expect(attributeAfterCall.x).toEqual(imageMeta.x);
+ expect(attributeAfterCall.y).toEqual(imageMeta.y);
+ expect(attributeAfterCall.width).toEqual(imageMeta.width);
+ expect(attributeAfterCall.height).toEqual(imageMeta.height);
+ });
+
+ it('should not override other properties', () => {
+ expect(attributeAfterCall.myProperty).toEqual('myProperty');
+ });
+ });
+
+ describe('updateDiscussionAvatarBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <a href="#" class="image-diff-avatar-link">
+ <div class="badge"></div>
+ </a>
+ `;
+ domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update avatar badge number', () => {
+ expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('updateDiscussionBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <div class="badge"></div>
+ `;
+ domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update discussion badge number', () => {
+ expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('toggleCollapsed', () => {
+ let element;
+ let discussionNotesEl;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.innerHTML = `
+ <div class="discussion-notes">
+ <button></button>
+ <form class="discussion-form"></form>
+ </div>
+ `;
+ discussionNotesEl = element.querySelector('.discussion-notes');
+ });
+
+ describe('not collapsed', () => {
+ beforeEach(() => {
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should add collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true);
+ });
+
+ it('should force formEl to display none', () => {
+ const formEl = element.querySelector('.discussion-form');
+ expect(formEl.style.display).toEqual('none');
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(() => {
+ discussionNotesEl.classList.add('collapsed');
+
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should remove collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false);
+ });
+
+ it('should force formEl to display block', () => {
+ const formEl = element.querySelector('.discussion-form');
+ expect(formEl.style.display).toEqual('block');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
new file mode 100644
index 00000000000..56d77a05c4c
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
@@ -0,0 +1,207 @@
+import * as utilsHelper from '~/image_diff/helpers/utils_helper';
+import ImageDiff from '~/image_diff/image_diff';
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageBadge from '~/image_diff/image_badge';
+import * as mockData from '../mock_data';
+
+describe('utilsHelper', () => {
+ const {
+ noteId,
+ discussionId,
+ image,
+ imageProperties,
+ imageMeta,
+ } = mockData;
+
+ describe('resizeCoordinatesToImageElement', () => {
+ let result;
+
+ beforeEach(() => {
+ result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta);
+ });
+
+ it('should return x based on widthRatio', () => {
+ expect(result.x).toEqual(imageMeta.x * 0.5);
+ });
+
+ it('should return y based on heightRatio', () => {
+ expect(result.y).toEqual(imageMeta.y * 0.5);
+ });
+
+ it('should return image width', () => {
+ expect(result.width).toEqual(image.width);
+ });
+
+ it('should return image height', () => {
+ expect(result.height).toEqual(image.height);
+ });
+ });
+
+ describe('generateBadgeFromDiscussionDOM', () => {
+ let discussionEl;
+ let result;
+
+ beforeEach(() => {
+ const imageFrameEl = document.createElement('div');
+ imageFrameEl.innerHTML = `
+ <img src="${gl.TEST_HOST}/image.png">
+ `;
+ discussionEl = document.createElement('div');
+ discussionEl.dataset.discussionId = discussionId;
+ discussionEl.innerHTML = `
+ <div class="note" id="${noteId}"></div>
+ `;
+ discussionEl.dataset.position = JSON.stringify(imageMeta);
+ result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl);
+ });
+
+ it('should return actual image properties', () => {
+ const { actual } = result;
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should return browser image properties', () => {
+ const { browser } = result;
+ expect(browser.x).toBeDefined();
+ expect(browser.y).toBeDefined();
+ expect(browser.width).toBeDefined();
+ expect(browser.height).toBeDefined();
+ });
+
+ it('should return instance of ImageBadge', () => {
+ expect(result instanceof ImageBadge).toEqual(true);
+ });
+
+ it('should return noteId', () => {
+ expect(result.noteId).toEqual(noteId);
+ });
+
+ it('should return discussionId', () => {
+ expect(result.discussionId).toEqual(discussionId);
+ });
+ });
+
+ describe('getTargetSelection', () => {
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = {
+ querySelector: () => imageProperties,
+ };
+ });
+
+ function generateEvent(offsetX, offsetY) {
+ return {
+ currentTarget: containerEl,
+ offsetX,
+ offsetY,
+ };
+ }
+
+ it('should return browser properties', () => {
+ const event = generateEvent(25, 25);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { browser } = result;
+ expect(browser.x).toEqual(event.offsetX);
+ expect(browser.y).toEqual(event.offsetY);
+ expect(browser.width).toEqual(imageProperties.width);
+ expect(browser.height).toEqual(imageProperties.height);
+ });
+
+ it('should return resized actual image properties', () => {
+ const event = generateEvent(50, 50);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { actual } = result;
+ expect(actual.x).toEqual(100);
+ expect(actual.y).toEqual(100);
+ expect(actual.width).toEqual(imageProperties.naturalWidth);
+ expect(actual.height).toEqual(imageProperties.naturalHeight);
+ });
+
+ describe('normalize coordinates', () => {
+ it('should return x = 0 if x < 0', () => {
+ const event = generateEvent(-5, 50);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.x).toEqual(0);
+ });
+
+ it('should return x = width if x > width', () => {
+ const event = generateEvent(1000, 50);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.x).toEqual(imageProperties.width);
+ });
+
+ it('should return y = 0 if y < 0', () => {
+ const event = generateEvent(50, -10);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.y).toEqual(0);
+ });
+
+ it('should return y = height if y > height', () => {
+ const event = generateEvent(50, 1000);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.y).toEqual(imageProperties.height);
+ });
+ });
+ });
+
+ describe('initImageDiff', () => {
+ let glCache;
+ let fileEl;
+
+ beforeEach(() => {
+ window.gl = window.gl || (window.gl = {});
+ glCache = window.gl;
+ window.gl.ImageFile = () => {};
+ fileEl = document.createElement('div');
+ fileEl.innerHTML = `
+ <div class="diff-file"></div>
+ `;
+
+ spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
+ spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {});
+ });
+
+ afterEach(() => {
+ window.gl = glCache;
+ });
+
+ it('should initialize gl.ImageFile', () => {
+ spyOn(window.gl, 'ImageFile');
+
+ utilsHelper.initImageDiff(fileEl, false, false);
+ expect(gl.ImageFile).toHaveBeenCalled();
+ });
+
+ it('should initialize ImageDiff if js-single-image', () => {
+ const diffFileEl = fileEl.querySelector('.diff-file');
+ diffFileEl.innerHTML = `
+ <div class="js-single-image">
+ </div>
+ `;
+
+ const imageDiff = utilsHelper.initImageDiff(fileEl, true, false);
+ expect(ImageDiff.prototype.init).toHaveBeenCalled();
+ expect(imageDiff.canCreateNote).toEqual(true);
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+
+ it('should initialize ReplacedImageDiff if js-replaced-image', () => {
+ const diffFileEl = fileEl.querySelector('.diff-file');
+ diffFileEl.innerHTML = `
+ <div class="js-replaced-image">
+ </div>
+ `;
+
+ const replacedImageDiff = utilsHelper.initImageDiff(fileEl, false, true);
+ expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled();
+ expect(replacedImageDiff.canCreateNote).toEqual(false);
+ expect(replacedImageDiff.renderCommentBadge).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js
new file mode 100644
index 00000000000..87f98fc0926
--- /dev/null
+++ b/spec/javascripts/image_diff/image_badge_spec.js
@@ -0,0 +1,84 @@
+import ImageBadge from '~/image_diff/image_badge';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageBadge', () => {
+ const { noteId, discussionId, imageMeta } = mockData;
+ const options = {
+ noteId,
+ discussionId,
+ };
+
+ it('should save actual property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ actual: imageMeta,
+ }));
+
+ const { actual } = imageBadge;
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should save browser property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ browser: imageMeta,
+ }));
+
+ const { browser } = imageBadge;
+ expect(browser.x).toEqual(imageMeta.x);
+ expect(browser.y).toEqual(imageMeta.y);
+ expect(browser.width).toEqual(imageMeta.width);
+ expect(browser.height).toEqual(imageMeta.height);
+ });
+
+ it('should save noteId', () => {
+ const imageBadge = new ImageBadge(options);
+ expect(imageBadge.noteId).toEqual(noteId);
+ });
+
+ it('should save discussionId', () => {
+ const imageBadge = new ImageBadge(options);
+ expect(imageBadge.discussionId).toEqual(discussionId);
+ });
+
+ describe('default values', () => {
+ let imageBadge;
+
+ beforeEach(() => {
+ imageBadge = new ImageBadge(options);
+ });
+
+ it('should return defaultimageMeta if actual property is not provided', () => {
+ const { actual } = imageBadge;
+ expect(actual.x).toEqual(0);
+ expect(actual.y).toEqual(0);
+ expect(actual.width).toEqual(0);
+ expect(actual.height).toEqual(0);
+ });
+
+ it('should return defaultimageMeta if browser property is not provided', () => {
+ const { browser } = imageBadge;
+ expect(browser.x).toEqual(0);
+ expect(browser.y).toEqual(0);
+ expect(browser.width).toEqual(0);
+ expect(browser.height).toEqual(0);
+ });
+ });
+
+ describe('imageEl property is provided and not browser property', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true);
+ });
+
+ it('should generate browser property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ imageEl: document.createElement('img'),
+ }));
+
+ expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled();
+ expect(imageBadge.browser).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js
new file mode 100644
index 00000000000..346282328c7
--- /dev/null
+++ b/spec/javascripts/image_diff/image_diff_spec.js
@@ -0,0 +1,361 @@
+import ImageDiff from '~/image_diff/image_diff';
+import * as imageUtility from '~/lib/utils/image_utility';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageDiff', () => {
+ let element;
+ let imageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="diff-file">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ <div class="comment-indicator"></div>
+ <div id="badge-1" class="badge">1</div>
+ <div id="badge-2" class="badge">2</div>
+ <div id="badge-3" class="badge">3</div>
+ </div>
+ <div class="note-container">
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ describe('constructor', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ renderCommentBadge: true,
+ });
+ });
+
+ it('should set el', () => {
+ expect(imageDiff.el).toEqual(element);
+ });
+
+ it('should set canCreateNote', () => {
+ expect(imageDiff.canCreateNote).toEqual(true);
+ });
+
+ it('should set renderCommentBadge', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(true);
+ });
+
+ it('should set $noteContainer', () => {
+ expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container'));
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element);
+ });
+
+ it('should set canCreateNote as false', () => {
+ expect(imageDiff.canCreateNote).toEqual(false);
+ });
+
+ it('should set renderCommentBadge as false', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+ });
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.init();
+ });
+
+ it('should set imageFrameEl', () => {
+ expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame'));
+ });
+
+ it('should set imageEl', () => {
+ expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img'));
+ });
+
+ it('should call bindEvents', () => {
+ expect(imageDiff.bindEvents).toHaveBeenCalled();
+ });
+ });
+
+ describe('bindEvents', () => {
+ let imageEl;
+
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+ imageEl = element.querySelector('.diff-file .js-image-frame img');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click event delegation to js-diff-notes-toggle', () => {
+ element.querySelector('.js-diff-notes-toggle').click();
+ expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled();
+ });
+
+ it('should register click event delegation to comment-indicator', () => {
+ element.querySelector('.comment-indicator').click();
+ expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled();
+ });
+ });
+
+ describe('image loaded', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(true);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ });
+
+ it('should renderBadges', () => {});
+ });
+
+ describe('image not loaded', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should registers load eventListener', () => {
+ const loadEvent = new Event('load');
+ imageEl.dispatchEvent(loadEvent);
+ expect(imageDiff.renderBadges).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ });
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.imageClicked).toHaveBeenCalled();
+ });
+
+ it('should register blur.imageDiff event', () => {
+ const event = new CustomEvent('blur.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should register addBadge.imageDiff event', () => {
+ const event = new CustomEvent('addBadge.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.addBadge).toHaveBeenCalled();
+ });
+
+ it('should register removeBadge.imageDiff event', () => {
+ const event = new CustomEvent('removeBadge.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.removeBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote is false', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should not register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.imageClicked).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('imageClicked', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({
+ actual: {},
+ browser: {},
+ });
+ spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageClicked({
+ detail: {
+ currentTarget: {},
+ },
+ });
+ });
+
+ it('should call getTargetSelection', () => {
+ expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled();
+ });
+
+ it('should call setPositionDataAttribute', () => {
+ expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled();
+ });
+
+ it('should call showCommentIndicator', () => {
+ expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderBadges', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadges();
+ });
+
+ it('should call renderBadge for each discussionEl', () => {
+ const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length);
+ });
+ });
+
+ describe('renderBadge', () => {
+ let discussionEls;
+
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({
+ browser: {},
+ noteId: 'noteId',
+ });
+ discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should populate imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ describe('renderCommentBadge', () => {
+ beforeEach(() => {
+ imageDiff.renderCommentBadge = true;
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should call addImageCommentBadge', () => {
+ expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderCommentBadge is false', () => {
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addBadge', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.addBadge({
+ detail: {
+ x: 0,
+ y: 1,
+ width: 25,
+ height: 50,
+ noteId: 'noteId',
+ discussionId: 'discussionId',
+ },
+ });
+ });
+
+ it('should add imageBadge to imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+
+ it('should call addAvatarBadge', () => {
+ expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ describe('removeBadge', () => {
+ beforeEach(() => {
+ const { imageMeta } = mockData;
+
+ spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.removeBadge({
+ detail: {
+ badgeNumber: 2,
+ },
+ });
+ });
+
+ describe('cascade badge count', () => {
+ it('should update next imageBadgeEl value', () => {
+ const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
+ expect(imageBadgeEls[0].innerText).toEqual('1');
+ expect(imageBadgeEls[1].innerText).toEqual('2');
+ expect(imageBadgeEls.length).toEqual(2);
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionAvatarBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ it('should remove badge from imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(2);
+ });
+
+ it('should remove imageBadgeEl', () => {
+ expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js
new file mode 100644
index 00000000000..7c447d6f70d
--- /dev/null
+++ b/spec/javascripts/image_diff/init_discussion_tab_spec.js
@@ -0,0 +1,37 @@
+import initDiscussionTab from '~/image_diff/init_discussion_tab';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('initDiscussionTab', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div class="timeline-content">
+ <div class="diff-file js-image-file"></div>
+ <div class="diff-file js-image-file"></div>
+ </div>
+ `);
+ });
+
+ it('should pass canCreateNote as false to initImageDiff', (done) => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => {
+ expect(canCreateNote).toEqual(false);
+ done();
+ });
+
+ initDiscussionTab();
+ });
+
+ it('should pass renderCommentBadge as true to initImageDiff', (done) => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote, renderCommentBadge) => {
+ expect(renderCommentBadge).toEqual(true);
+ done();
+ });
+
+ initDiscussionTab();
+ });
+
+ it('should call initImageDiff for each diffFileEls', () => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake(() => {});
+ initDiscussionTab();
+ expect(imageDiffHelper.initImageDiff.calls.count()).toEqual(2);
+ });
+});
diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/javascripts/image_diff/mock_data.js
new file mode 100644
index 00000000000..a0d1732dd0a
--- /dev/null
+++ b/spec/javascripts/image_diff/mock_data.js
@@ -0,0 +1,28 @@
+export const noteId = 'noteId';
+export const discussionId = 'discussionId';
+export const badgeText = 'badgeText';
+export const badgeNumber = 5;
+
+export const coordinate = {
+ x: 100,
+ y: 100,
+};
+
+export const image = {
+ width: 100,
+ height: 100,
+};
+
+export const imageProperties = {
+ width: image.width,
+ height: image.height,
+ naturalWidth: image.width * 2,
+ naturalHeight: image.height * 2,
+};
+
+export const imageMeta = {
+ x: coordinate.x,
+ y: coordinate.y,
+ width: imageProperties.naturalWidth,
+ height: imageProperties.naturalHeight,
+};
diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js
new file mode 100644
index 00000000000..5f8cd7c531a
--- /dev/null
+++ b/spec/javascripts/image_diff/replaced_image_diff_spec.js
@@ -0,0 +1,312 @@
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageDiff from '~/image_diff/image_diff';
+import { viewTypes } from '~/image_diff/view_types';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('ReplacedImageDiff', () => {
+ let element;
+ let replacedImageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="two-up">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="swipe">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="onion-skin">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="view-modes-menu">
+ <div class="two-up">2-up</div>
+ <div class="swipe">Swipe</div>
+ <div class="onion-skin">Onion skin</div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ function setupImageFrameEls() {
+ replacedImageDiff.imageFrameEls = [];
+ replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector('.two-up .js-image-frame');
+ replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector('.swipe .js-image-frame');
+ replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin .js-image-frame');
+ }
+
+ function setupViewModesEls() {
+ replacedImageDiff.viewModesEls = [];
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector('.view-modes-menu .two-up');
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector('.view-modes-menu .swipe');
+ replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector('.view-modes-menu .onion-skin');
+ }
+
+ function setupImageEls() {
+ replacedImageDiff.imageEls = [];
+ replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img');
+ replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img');
+ replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img');
+ }
+
+ it('should extend ImageDiff', () => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ expect(replacedImageDiff instanceof ImageDiff).toEqual(true);
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.init();
+ });
+
+ it('should set imageFrameEls', () => {
+ const { imageFrameEls } = replacedImageDiff;
+ expect(imageFrameEls).toBeDefined();
+ expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up .js-image-frame'));
+ expect(imageFrameEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe .js-image-frame'));
+ expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin .js-image-frame'));
+ });
+
+ it('should set viewModesEls', () => {
+ const { viewModesEls } = replacedImageDiff;
+ expect(viewModesEls).toBeDefined();
+ expect(viewModesEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.view-modes-menu .two-up'));
+ expect(viewModesEls[viewTypes.SWIPE]).toEqual(element.querySelector('.view-modes-menu .swipe'));
+ expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.view-modes-menu .onion-skin'));
+ });
+
+ it('should generateImageEls', () => {
+ expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled();
+ });
+
+ it('should bindEvents', () => {
+ expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ describe('currentView', () => {
+ it('should set currentView', () => {
+ replacedImageDiff.init(viewTypes.ONION_SKIN);
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should default to viewTypes.TWO_UP', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP);
+ });
+ });
+ });
+
+ describe('generateImageEls', () => {
+ beforeEach(() => {
+ spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element, {
+ canCreateNote: false,
+ renderCommentBadge: false,
+ });
+
+ setupImageFrameEls();
+ });
+
+ it('should set imageEls', () => {
+ replacedImageDiff.generateImageEls();
+ const { imageEls } = replacedImageDiff;
+ expect(imageEls).toBeDefined();
+ expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img'));
+ expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img'));
+ expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img'));
+ });
+ });
+
+ describe('bindEvents', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ setupViewModesEls();
+ });
+
+ it('should call super.bindEvents', () => {
+ replacedImageDiff.bindEvents();
+ expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ it('should register click eventlistener to 2-up view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.TWO_UP);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
+ });
+
+ it('should register click eventlistener to swipe view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+
+ it('should register click eventlistener to onion skin view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+ });
+
+ describe('getters', () => {
+ describe('imageEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageEls();
+ });
+
+ it('should return imageEl based on currentView', () => {
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img'));
+
+ replacedImageDiff.currentView = viewTypes.SWIPE;
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img'));
+ });
+ });
+
+ describe('imageFrameEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageFrameEls();
+ });
+
+ it('should return imageFrameEl based on currentView', () => {
+ expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.two-up .js-image-frame'));
+
+ replacedImageDiff.currentView = viewTypes.ONION_SKIN;
+ expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.onion-skin .js-image-frame'));
+ });
+ });
+ });
+
+ describe('changeView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({
+ removed: false,
+ });
+ setupImageFrameEls();
+ });
+
+ describe('invalid viewType', () => {
+ beforeEach(() => {
+ replacedImageDiff.changeView('some-view-name');
+ });
+
+ it('should not call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('valid viewType', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {});
+ replacedImageDiff.changeView(viewTypes.ONION_SKIN);
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ it('should call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should update currentView to newView', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should clear imageBadges', () => {
+ expect(replacedImageDiff.imageBadges.length).toEqual(0);
+ });
+
+ it('should call renderNewView', () => {
+ jasmine.clock().tick(251);
+ expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('renderNewView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ });
+
+ it('should call renderBadges', () => {
+ spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+
+ replacedImageDiff.renderNewView({
+ removed: false,
+ });
+
+ expect(replacedImageDiff.renderBadges).toHaveBeenCalled();
+ });
+
+ describe('removeIndicator', () => {
+ const indicator = {
+ removed: true,
+ x: 0,
+ y: 1,
+ image: {
+ width: 50,
+ height: 100,
+ },
+ };
+
+ beforeEach(() => {
+ setupImageEls();
+ setupImageFrameEls();
+ });
+
+ it('should pass showCommentIndicator normalized indicator values', (done) => {
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => {
+ expect(meta.x).toEqual(indicator.x);
+ expect(meta.y).toEqual(indicator.y);
+ expect(meta.width).toEqual(indicator.image.width);
+ expect(meta.height).toEqual(indicator.image.height);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+
+ it('should call showCommentIndicator', (done) => {
+ const normalized = {
+ normalized: true,
+ };
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized);
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake((imageFrameEl, normalizedIndicator) => {
+ expect(normalizedIndicator).toEqual(normalized);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/javascripts/image_diff/view_types_spec.js
new file mode 100644
index 00000000000..e9639f46497
--- /dev/null
+++ b/spec/javascripts/image_diff/view_types_spec.js
@@ -0,0 +1,24 @@
+import { viewTypes, isValidViewType } from '~/image_diff/view_types';
+
+describe('viewTypes', () => {
+ describe('isValidViewType', () => {
+ it('should return true for TWO_UP', () => {
+ expect(isValidViewType(viewTypes.TWO_UP)).toEqual(true);
+ });
+
+ it('should return true for SWIPE', () => {
+ expect(isValidViewType(viewTypes.SWIPE)).toEqual(true);
+ });
+
+ it('should return true for ONION_SKIN', () => {
+ expect(isValidViewType(viewTypes.ONION_SKIN)).toEqual(true);
+ });
+
+ it('should return false for non view types', () => {
+ expect(isValidViewType('some-view-type')).toEqual(false);
+ expect(isValidViewType(null)).toEqual(false);
+ expect(isValidViewType(undefined)).toEqual(false);
+ expect(isValidViewType('')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js
new file mode 100644
index 00000000000..bcb2b7b24a0
--- /dev/null
+++ b/spec/javascripts/issuable_context_spec.js
@@ -0,0 +1,34 @@
+/* global IssuableContext */
+import '~/issuable_context';
+import $ from 'jquery';
+
+describe('IssuableContext', () => {
+ describe('toggleHiddenParticipants', () => {
+ const event = jasmine.createSpyObj('event', ['preventDefault']);
+
+ beforeEach(() => {
+ spyOn($.fn, 'data').and.returnValue('data');
+ spyOn($.fn, 'text').and.returnValue('data');
+ });
+
+ afterEach(() => {
+ gl.lazyLoader = undefined;
+ });
+
+ it('calls loadCheck if lazyLoader is set', () => {
+ gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
+
+ IssuableContext.prototype.toggleHiddenParticipants(event);
+
+ expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
+ });
+
+ it('does not throw if lazyLoader is not defined', () => {
+ gl.lazyLoader = undefined;
+
+ const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
+
+ expect(toggle).not.toThrow();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 39065814bc2..583a3a74d77 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -42,7 +42,6 @@ describe('Issuable output', () => {
initialDescriptionText: '',
markdownPreviewPath: '/',
markdownDocsPath: '/',
- isConfidential: false,
projectNamespace: '/',
projectPath: '/',
},
@@ -157,30 +156,6 @@ describe('Issuable output', () => {
});
});
- it('reloads the page if the confidential status has changed', (done) => {
- spyOn(gl.utils, 'visitUrl');
- spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
- resolve({
- json() {
- return {
- confidential: true,
- web_url: location.pathname,
- };
- },
- });
- }));
-
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(
- gl.utils.visitUrl,
- ).toHaveBeenCalledWith(location.pathname);
-
- done();
- });
- });
-
it('correctly updates issuable data', (done) => {
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve();
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index a6ad250bd86..f86f2f260c3 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -1,398 +1,481 @@
/* eslint-disable promise/catch-or-return */
-import '~/lib/utils/common_utils';
+import * as commonUtils from '~/lib/utils/common_utils';
-(() => {
- describe('common_utils', () => {
- describe('gl.utils.parseUrl', () => {
- it('returns an anchor tag with url', () => {
- expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
- });
- it('url is escaped', () => {
- // IE11 will return a relative pathname while other browsers will return a full pathname.
- // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
- // element will create an absolute url relative to the current execution context.
- // The JavaScript test suite is executed at '/' which will lead to an absolute url
- // starting with '/'.
- expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
- });
+describe('common_utils', () => {
+ describe('parseUrl', () => {
+ it('returns an anchor tag with url', () => {
+ expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
});
+ it('url is escaped', () => {
+ // IE11 will return a relative pathname while other browsers will return a full pathname.
+ // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
+ // element will create an absolute url relative to the current execution context.
+ // The JavaScript test suite is executed at '/' which will lead to an absolute url
+ // starting with '/'.
+ expect(commonUtils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
+ });
+ });
- describe('gl.utils.parseUrlPathname', () => {
- beforeEach(() => {
- spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
- pathname: url,
- }));
- });
- it('returns an absolute url when given an absolute url', () => {
- expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
- });
- it('returns an absolute url when given a relative url', () => {
- expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
- });
+ describe('parseUrlPathname', () => {
+ it('returns an absolute url when given an absolute url', () => {
+ expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
});
- describe('gl.utils.getUrlParamsArray', () => {
- it('should return params array', () => {
- expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true);
- });
+ it('returns an absolute url when given a relative url', () => {
+ expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
+ });
+ });
- it('should remove the question mark from the search params', () => {
- const paramsArray = gl.utils.getUrlParamsArray();
- expect(paramsArray[0][0] !== '?').toBe(true);
- });
+ describe('getUrlParamsArray', () => {
+ it('should return params array', () => {
+ expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true);
+ });
+
+ it('should remove the question mark from the search params', () => {
+ const paramsArray = commonUtils.getUrlParamsArray();
+ expect(paramsArray[0][0] !== '?').toBe(true);
+ });
- it('should decode params', () => {
- history.pushState('', '', '?label_name%5B%5D=test');
+ it('should decode params', () => {
+ history.pushState('', '', '?label_name%5B%5D=test');
- expect(
- gl.utils.getUrlParamsArray()[0],
- ).toBe('label_name[]=test');
+ expect(
+ commonUtils.getUrlParamsArray()[0],
+ ).toBe('label_name[]=test');
- history.pushState('', '', '?');
- });
+ history.pushState('', '', '?');
});
+ });
- describe('gl.utils.handleLocationHash', () => {
- beforeEach(() => {
- spyOn(window.document, 'getElementById').and.callThrough();
- });
+ describe('handleLocationHash', () => {
+ beforeEach(() => {
+ spyOn(window.document, 'getElementById').and.callThrough();
+ });
- afterEach(() => {
- window.history.pushState({}, null, '');
- });
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
- function expectGetElementIdToHaveBeenCalledWith(elementId) {
- expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
- }
+ function expectGetElementIdToHaveBeenCalledWith(elementId) {
+ expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
+ }
- it('decodes hash parameter', () => {
- window.history.pushState({}, null, '#random-hash');
- gl.utils.handleLocationHash();
+ it('decodes hash parameter', () => {
+ window.history.pushState({}, null, '#random-hash');
+ commonUtils.handleLocationHash();
- expectGetElementIdToHaveBeenCalledWith('random-hash');
- expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
- });
+ expectGetElementIdToHaveBeenCalledWith('random-hash');
+ expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
+ });
- it('decodes cyrillic hash parameter', () => {
- window.history.pushState({}, null, '#definição');
- gl.utils.handleLocationHash();
+ it('decodes cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#definição');
+ commonUtils.handleLocationHash();
- expectGetElementIdToHaveBeenCalledWith('definição');
- expectGetElementIdToHaveBeenCalledWith('user-content-definição');
- });
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
- it('decodes encoded cyrillic hash parameter', () => {
- window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
- gl.utils.handleLocationHash();
+ it('decodes encoded cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
+ commonUtils.handleLocationHash();
- expectGetElementIdToHaveBeenCalledWith('definição');
- expectGetElementIdToHaveBeenCalledWith('user-content-definição');
- });
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
});
- describe('gl.utils.setParamInURL', () => {
- afterEach(() => {
- window.history.pushState({}, null, '');
- });
+ it('scrolls element into view', () => {
+ document.body.innerHTML += `
+ <div id="parent">
+ <div style="height: 2000px;"></div>
+ <div id="test" style="height: 2000px;"></div>
+ </div>
+ `;
- it('should return the parameter', () => {
- window.history.replaceState({}, null, '');
+ window.history.pushState({}, null, '#test');
+ commonUtils.handleLocationHash();
- expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156');
- expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156');
- });
+ expectGetElementIdToHaveBeenCalledWith('test');
+ expect(window.scrollY).toBe(document.getElementById('test').offsetTop);
- it('should update the existing parameter when its a number', () => {
- window.history.pushState({}, null, '?page=15');
+ document.getElementById('parent').remove();
+ });
- expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16');
- expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16');
- expect(gl.utils.setParamInURL('page', true)).toBe('?page=true');
- });
+ it('scrolls user content element into view', () => {
+ document.body.innerHTML += `
+ <div id="parent">
+ <div style="height: 2000px;"></div>
+ <div id="user-content-test" style="height: 2000px;"></div>
+ </div>
+ `;
- it('should update the existing parameter when its a string', () => {
- window.history.pushState({}, null, '?scope=all');
+ window.history.pushState({}, null, '#test');
+ commonUtils.handleLocationHash();
- expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished');
- });
+ expectGetElementIdToHaveBeenCalledWith('test');
+ expectGetElementIdToHaveBeenCalledWith('user-content-test');
+ expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop);
- it('should update the existing parameter when more than one parameter exists', () => {
- window.history.pushState({}, null, '?scope=all&page=15');
+ document.getElementById('parent').remove();
+ });
- expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15');
- });
+ it('scrolls to element with offset from navbar', () => {
+ spyOn(window, 'scrollBy').and.callThrough();
+ document.body.innerHTML += `
+ <div id="parent">
+ <div class="navbar-gitlab" style="position: fixed; top: 0; height: 50px;"></div>
+ <div style="height: 2000px; margin-top: 50px;"></div>
+ <div id="user-content-test" style="height: 2000px;"></div>
+ </div>
+ `;
+
+ window.history.pushState({}, null, '#test');
+ commonUtils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('test');
+ expectGetElementIdToHaveBeenCalledWith('user-content-test');
+ expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop - 50);
+ expect(window.scrollBy).toHaveBeenCalledWith(0, -50);
+
+ document.getElementById('parent').remove();
+ });
+ });
+
+ describe('setParamInURL', () => {
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
- it('should add a new parameter to the end of the existing ones', () => {
- window.history.pushState({}, null, '?scope=all');
+ it('should return the parameter', () => {
+ window.history.replaceState({}, null, '');
- expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16');
- expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16');
- expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true');
- });
+ expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156');
+ expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156');
});
- describe('gl.utils.getParameterByName', () => {
- beforeEach(() => {
- window.history.pushState({}, null, '?scope=all&p=2');
- });
+ it('should update the existing parameter when its a number', () => {
+ window.history.pushState({}, null, '?page=15');
- afterEach(() => {
- window.history.replaceState({}, null, null);
- });
+ expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16');
+ expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16');
+ expect(commonUtils.setParamInURL('page', true)).toBe('?page=true');
+ });
- it('should return valid parameter', () => {
- const value = gl.utils.getParameterByName('scope');
- expect(gl.utils.getParameterByName('p')).toEqual('2');
- expect(value).toBe('all');
- });
+ it('should update the existing parameter when its a string', () => {
+ window.history.pushState({}, null, '?scope=all');
- it('should return invalid parameter', () => {
- const value = gl.utils.getParameterByName('fakeParameter');
- expect(value).toBe(null);
- });
+ expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished');
+ });
- it('should return valid paramentes if URL is provided', () => {
- let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar');
- expect(value).toBe('bar');
+ it('should update the existing parameter when more than one parameter exists', () => {
+ window.history.pushState({}, null, '?scope=all&page=15');
- value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu');
- expect(value).toBe('canchu');
- });
+ expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15');
});
- describe('gl.utils.normalizedHeaders', () => {
- it('should upperCase all the header keys to keep them consistent', () => {
- const apiHeaders = {
- 'X-Something-Workhorse': { workhorse: 'ok' },
- 'x-something-nginx': { nginx: 'ok' },
- };
+ it('should add a new parameter to the end of the existing ones', () => {
+ window.history.pushState({}, null, '?scope=all');
- const normalized = gl.utils.normalizeHeaders(apiHeaders);
+ expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16');
+ expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16');
+ expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true');
+ });
+ });
- const WORKHORSE = 'X-SOMETHING-WORKHORSE';
- const NGINX = 'X-SOMETHING-NGINX';
+ describe('getParameterByName', () => {
+ beforeEach(() => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+ });
- expect(normalized[WORKHORSE].workhorse).toBe('ok');
- expect(normalized[NGINX].nginx).toBe('ok');
- });
+ afterEach(() => {
+ window.history.replaceState({}, null, null);
});
- describe('gl.utils.normalizeCRLFHeaders', () => {
- beforeEach(function () {
- this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
+ it('should return valid parameter', () => {
+ const value = commonUtils.getParameterByName('scope');
+ expect(commonUtils.getParameterByName('p')).toEqual('2');
+ expect(value).toBe('all');
+ });
- spyOn(String.prototype, 'split').and.callThrough();
- spyOn(gl.utils, 'normalizeHeaders').and.callThrough();
+ it('should return invalid parameter', () => {
+ const value = commonUtils.getParameterByName('fakeParameter');
+ expect(value).toBe(null);
+ });
- this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders);
- });
+ it('should return valid paramentes if URL is provided', () => {
+ let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar');
+ expect(value).toBe('bar');
- it('should split by newline', function () {
- expect(String.prototype.split).toHaveBeenCalledWith('\n');
- });
+ value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu');
+ expect(value).toBe('canchu');
+ });
+ });
- it('should split by colon+space for each header', function () {
- expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
- });
+ describe('normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
- it('should call gl.utils.normalizeHeaders with a parsed headers object', function () {
- expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object));
- });
+ const normalized = commonUtils.normalizeHeaders(apiHeaders);
- it('should return a normalized headers object', function () {
- expect(this.normalizeCRLFHeaders).toEqual({
- 'A-HEADER': 'a-value',
- 'ANOTHER-HEADER': 'ANOTHER-VALUE',
- 'LAST-HEADER': 'last-VALUE',
- });
- });
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
});
+ });
- describe('gl.utils.parseIntPagination', () => {
- it('should parse to integers all string values and return pagination object', () => {
- const pagination = {
- 'X-PER-PAGE': 10,
- 'X-PAGE': 2,
- 'X-TOTAL': 30,
- 'X-TOTAL-PAGES': 3,
- 'X-NEXT-PAGE': 3,
- 'X-PREV-PAGE': 1,
- };
-
- const expectedPagination = {
- perPage: 10,
- page: 2,
- total: 30,
- totalPages: 3,
- nextPage: 3,
- previousPage: 1,
- };
-
- expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination);
- });
+ describe('normalizeCRLFHeaders', () => {
+ beforeEach(function () {
+ this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
+ spyOn(String.prototype, 'split').and.callThrough();
+ this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders);
+ });
+
+ it('should split by newline', function () {
+ expect(String.prototype.split).toHaveBeenCalledWith('\n');
});
- describe('gl.utils.isMetaClick', () => {
- it('should identify meta click on Windows/Linux', () => {
- const e = {
- metaKey: false,
- ctrlKey: true,
- which: 1,
- };
+ it('should split by colon+space for each header', function () {
+ expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
+ });
- expect(gl.utils.isMetaClick(e)).toBe(true);
+ it('should return a normalized headers object', function () {
+ expect(this.normalizeCRLFHeaders).toEqual({
+ 'A-HEADER': 'a-value',
+ 'ANOTHER-HEADER': 'ANOTHER-VALUE',
+ 'LAST-HEADER': 'last-VALUE',
});
+ });
+ });
+
+ describe('parseIntPagination', () => {
+ it('should parse to integers all string values and return pagination object', () => {
+ const pagination = {
+ 'X-PER-PAGE': 10,
+ 'X-PAGE': 2,
+ 'X-TOTAL': 30,
+ 'X-TOTAL-PAGES': 3,
+ 'X-NEXT-PAGE': 3,
+ 'X-PREV-PAGE': 1,
+ };
+
+ const expectedPagination = {
+ perPage: 10,
+ page: 2,
+ total: 30,
+ totalPages: 3,
+ nextPage: 3,
+ previousPage: 1,
+ };
+
+ expect(commonUtils.parseIntPagination(pagination)).toEqual(expectedPagination);
+ });
+ });
- it('should identify meta click on macOS', () => {
- const e = {
- metaKey: true,
- ctrlKey: false,
- which: 1,
- };
+ describe('isMetaClick', () => {
+ it('should identify meta click on Windows/Linux', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ };
- expect(gl.utils.isMetaClick(e)).toBe(true);
- });
+ expect(commonUtils.isMetaClick(e)).toBe(true);
+ });
- it('should identify as meta click on middle-click or Mouse-wheel click', () => {
- const e = {
- metaKey: false,
- ctrlKey: false,
- which: 2,
- };
+ it('should identify meta click on macOS', () => {
+ const e = {
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ };
- expect(gl.utils.isMetaClick(e)).toBe(true);
+ expect(commonUtils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify as meta click on middle-click or Mouse-wheel click', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ };
+
+ expect(commonUtils.isMetaClick(e)).toBe(true);
+ });
+ });
+
+ describe('convertPermissionToBoolean', () => {
+ it('should convert a boolean in a string to a boolean', () => {
+ expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true);
+ expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false);
+ });
+ });
+
+ describe('backOff', () => {
+ beforeEach(() => {
+ // shortcut our timeouts otherwise these tests will take a long time to finish
+ const origSetTimeout = window.setTimeout;
+ spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
+ });
+
+ it('solves the promise from the callback', (done) => {
+ const expectedResponseValue = 'Success!';
+ commonUtils.backOff((next, stop) => (
+ new Promise((resolve) => {
+ resolve(expectedResponseValue);
+ }).then((resp) => {
+ stop(resp);
+ })
+ )).then((respBackoff) => {
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
});
});
- describe('gl.utils.backOff', () => {
- beforeEach(() => {
- // shortcut our timeouts otherwise these tests will take a long time to finish
- const origSetTimeout = window.setTimeout;
- spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
+ it('catches the rejected promise from the callback ', (done) => {
+ const errorMessage = 'Mistakes were made!';
+ commonUtils.backOff((next, stop) => {
+ new Promise((resolve, reject) => {
+ reject(new Error(errorMessage));
+ }).then((resp) => {
+ stop(resp);
+ }).catch(err => stop(err));
+ }).catch((errBackoffResp) => {
+ expect(errBackoffResp instanceof Error).toBe(true);
+ expect(errBackoffResp.message).toBe(errorMessage);
+ done();
});
+ });
- it('solves the promise from the callback', (done) => {
- const expectedResponseValue = 'Success!';
- gl.utils.backOff((next, stop) => (
- new Promise((resolve) => {
- resolve(expectedResponseValue);
- }).then((resp) => {
- stop(resp);
+ it('solves the promise correctly after retrying a third time', (done) => {
+ let numberOfCalls = 1;
+ const expectedResponseValue = 'Success!';
+ commonUtils.backOff((next, stop) => (
+ Promise.resolve(expectedResponseValue)
+ .then((resp) => {
+ if (numberOfCalls < 3) {
+ numberOfCalls += 1;
+ next();
+ } else {
+ stop(resp);
+ }
})
- )).then((respBackoff) => {
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- });
+ )).then((respBackoff) => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ expect(timeouts).toEqual([2000, 4000]);
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
});
+ });
- it('catches the rejected promise from the callback ', (done) => {
- const errorMessage = 'Mistakes were made!';
- gl.utils.backOff((next, stop) => {
- new Promise((resolve, reject) => {
- reject(new Error(errorMessage));
- }).then((resp) => {
- stop(resp);
- }).catch(err => stop(err));
- }).catch((errBackoffResp) => {
+ it('rejects the backOff promise after timing out', (done) => {
+ commonUtils.backOff(next => next(), 64000)
+ .catch((errBackoffResp) => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe(errorMessage);
+ expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
done();
});
- });
+ });
+ });
- it('solves the promise correctly after retrying a third time', (done) => {
- let numberOfCalls = 1;
- const expectedResponseValue = 'Success!';
- gl.utils.backOff((next, stop) => (
- Promise.resolve(expectedResponseValue)
- .then((resp) => {
- if (numberOfCalls < 3) {
- numberOfCalls += 1;
- next();
- } else {
- stop(resp);
- }
- })
- )).then((respBackoff) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000]);
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- });
- });
+ describe('setFavicon', () => {
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('href', 'default/favicon');
+ document.body.appendChild(favicon);
+ });
- it('rejects the backOff promise after timing out', (done) => {
- gl.utils.backOff(next => next(), 64000)
- .catch((errBackoffResp) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
- done();
- });
- });
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
});
+ it('should set page favicon to provided favicon', () => {
+ const faviconPath = '//custom_favicon';
+ commonUtils.setFavicon(faviconPath);
- describe('gl.utils.setFavicon', () => {
- it('should set page favicon to provided favicon', () => {
- const faviconPath = '//custom_favicon';
- const fakeLink = {
- setAttribute() {},
- };
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconPath);
+ });
+ });
- spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
- spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
- expect(attr).toEqual('href');
- expect(val.indexOf(faviconPath) > -1).toBe(true);
- });
- gl.utils.setFavicon(faviconPath);
- });
+ describe('resetFavicon', () => {
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('href', 'default/favicon');
+ document.body.appendChild(favicon);
});
- describe('gl.utils.resetFavicon', () => {
- it('should reset page favicon to tanuki', () => {
- const fakeLink = {
- setAttribute() {},
- };
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
+ });
- spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
- spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
- expect(attr).toEqual('href');
- expect(val).toMatch(/favicon/);
- });
- gl.utils.resetFavicon();
+ it('should reset page favicon to tanuki', () => {
+ commonUtils.resetFavicon();
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon');
+ });
+ });
+
+ describe('setCiStatusFavicon', () => {
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
+
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ document.body.appendChild(favicon);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
+ });
+
+ it('should reset favicon in case of error', () => {
+ const favicon = document.getElementById('favicon');
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.error();
+ expect(favicon.getAttribute('href')).toEqual('null');
});
+
+ commonUtils.setCiStatusFavicon(BUILD_URL);
});
- 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/-/jobs/1/status.json`;
- 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({ favicon: FAVICON_PATH });
- expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH);
- options.success();
- expect(spyResetFavicon).toHaveBeenCalled();
- options.error();
- expect(spyResetFavicon).toHaveBeenCalled();
- });
+ it('should set page favicon to CI status favicon based on provided status', () => {
+ const FAVICON_PATH = '//icon_status_success';
+ const favicon = document.getElementById('favicon');
- gl.utils.setCiStatusFavicon(BUILD_URL);
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ favicon: FAVICON_PATH });
+ expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH);
});
+
+ commonUtils.setCiStatusFavicon(BUILD_URL);
});
+ });
- describe('gl.utils.ajaxPost', () => {
- it('should perform `$.ajax` call and do `POST` request', () => {
- const requestURL = '/some/random/api';
- const data = { keyname: 'value' };
- const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
+ describe('ajaxPost', () => {
+ it('should perform `$.ajax` call and do `POST` request', () => {
+ const requestURL = '/some/random/api';
+ const data = { keyname: 'value' };
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
+
+ commonUtils.ajaxPost(requestURL, data);
+ expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
+ });
+
+ describe('gl.utils.spriteIcon', () => {
+ beforeEach(() => {
+ window.gon.sprite_icons = 'icons.svg';
+ });
- gl.utils.ajaxPost(requestURL, data);
- expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
+ it('should return the svg for a linked icon', () => {
+ expect(gl.utils.spriteIcon('test')).toEqual('<svg><use xlink:href="icons.svg#test" /></svg>');
});
});
});
-})();
+});
diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js
new file mode 100644
index 00000000000..c484213df8e
--- /dev/null
+++ b/spec/javascripts/lib/utils/csrf_token_spec.js
@@ -0,0 +1,49 @@
+import csrf from '~/lib/utils/csrf';
+
+describe('csrf', () => {
+ beforeEach(() => {
+ this.tokenKey = 'X-CSRF-Token';
+ this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
+ });
+
+ it('returns the correct headerKey', () => {
+ expect(csrf.headerKey).toBe(this.tokenKey);
+ });
+
+ describe('when csrf token is in the DOM', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <meta name="csrf-token" content="${this.token}">
+ `);
+
+ csrf.init();
+ });
+
+ it('returns the csrf token', () => {
+ expect(csrf.token).toBe(this.token);
+ });
+
+ it('returns the csrf headers object', () => {
+ expect(csrf.headers[this.tokenKey]).toBe(this.token);
+ });
+ });
+
+ describe('when csrf token is not in the DOM', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <meta name="some-other-token">
+ `);
+
+ csrf.init();
+ });
+
+ it('returns null for token', () => {
+ expect(csrf.token).toBeNull();
+ });
+
+ it('returns empty object for headers', () => {
+ expect(typeof csrf.headers).toBe('object');
+ expect(Object.keys(csrf.headers).length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js
new file mode 100644
index 00000000000..75addfcc833
--- /dev/null
+++ b/spec/javascripts/lib/utils/image_utility_spec.js
@@ -0,0 +1,32 @@
+import * as imageUtility from '~/lib/utils/image_utility';
+
+describe('imageUtility', () => {
+ describe('isImageLoaded', () => {
+ it('should return false when image.complete is false', () => {
+ const element = {
+ complete: false,
+ naturalHeight: 100,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ });
+
+ it('should return false when naturalHeight = 0', () => {
+ const element = {
+ complete: true,
+ naturalHeight: 0,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ });
+
+ it('should return true when image.complete and naturalHeight != 0', () => {
+ const element = {
+ complete: true,
+ naturalHeight: 100,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js
index c3ee3ef9825..b87c836654d 100644
--- a/spec/javascripts/lib/utils/sticky_spec.js
+++ b/spec/javascripts/lib/utils/sticky_spec.js
@@ -1,52 +1,79 @@
import { isSticky } from '~/lib/utils/sticky';
describe('sticky', () => {
- const el = {
- offsetTop: 0,
- classList: {},
- };
+ let el;
beforeEach(() => {
- el.offsetTop = 0;
- el.classList.add = jasmine.createSpy('spy');
- el.classList.remove = jasmine.createSpy('spy');
+ document.body.innerHTML += `
+ <div class="parent">
+ <div id="js-sticky"></div>
+ </div>
+ `;
+
+ el = document.getElementById('js-sticky');
});
- describe('classList.remove', () => {
- it('does not call classList.remove when stuck', () => {
- isSticky(el, 0, 0);
+ afterEach(() => {
+ el.parentNode.remove();
+ });
+
+ describe('when stuck', () => {
+ it('does not remove is-stuck class', () => {
+ isSticky(el, 0, el.offsetTop);
+ isSticky(el, 0, el.offsetTop);
expect(
- el.classList.remove,
- ).not.toHaveBeenCalled();
+ el.classList.contains('is-stuck'),
+ ).toBeTruthy();
});
- it('calls classList.remove when not stuck', () => {
- el.offsetTop = 10;
- isSticky(el, 0, 0);
+ it('adds is-stuck class', () => {
+ isSticky(el, 0, el.offsetTop);
expect(
- el.classList.remove,
- ).toHaveBeenCalledWith('is-stuck');
+ el.classList.contains('is-stuck'),
+ ).toBeTruthy();
+ });
+
+ it('inserts placeholder element', () => {
+ isSticky(el, 0, el.offsetTop, true);
+
+ expect(
+ document.querySelector('.sticky-placeholder'),
+ ).not.toBeNull();
});
});
- describe('classList.add', () => {
- it('calls classList.add when stuck', () => {
+ describe('when not stuck', () => {
+ it('removes is-stuck class', () => {
+ spyOn(el.classList, 'remove').and.callThrough();
+
+ isSticky(el, 0, el.offsetTop);
isSticky(el, 0, 0);
expect(
- el.classList.add,
+ el.classList.remove,
).toHaveBeenCalledWith('is-stuck');
+ expect(
+ el.classList.contains('is-stuck'),
+ ).toBeFalsy();
});
- it('does not call classList.add when not stuck', () => {
- el.offsetTop = 10;
+ it('does not add is-stuck class', () => {
isSticky(el, 0, 0);
expect(
- el.classList.add,
- ).not.toHaveBeenCalled();
+ el.classList.contains('is-stuck'),
+ ).toBeFalsy();
+ });
+
+ it('removes placeholder', () => {
+ isSticky(el, 0, el.offsetTop, true);
+ isSticky(el, 0, 0, true);
+
+ expect(
+ document.querySelector('.sticky-placeholder'),
+ ).toBeNull();
});
});
});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index aee274641e8..645664a5219 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -18,19 +18,25 @@ import '~/line_highlighter';
beforeEach(function() {
loadFixtures('static/line_highlighter.html.raw');
this["class"] = new LineHighlighter();
- this.css = this["class"].highlightClass;
+ this.css = this["class"].highlightLineClass;
return this.spies = {
__setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {})
};
});
describe('behavior', function() {
it('highlights one line given in the URL hash', function() {
- new LineHighlighter('#L13');
+ new LineHighlighter({ hash: '#L13' });
return expect($('#LC13')).toHaveClass(this.css);
});
+ it('highlights one line given in the URL hash with given CSS class name', function() {
+ const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' });
+ expect(hiliter.highlightLineClass).toBe('hilite');
+ expect($('#LC13')).toHaveClass('hilite');
+ expect($('#LC13')).not.toHaveClass('hll');
+ });
it('highlights a range of lines given in the URL hash', function() {
var line, results;
- new LineHighlighter('#L5-25');
+ new LineHighlighter({ hash: '#L5-25' });
expect($("." + this.css).length).toBe(21);
results = [];
for (line = 5; line <= 25; line += 1) {
@@ -41,7 +47,7 @@ import '~/line_highlighter';
it('scrolls to the first highlighted line on initial load', function() {
var spy;
spy = spyOn($, 'scrollTo');
- new LineHighlighter('#L5-25');
+ new LineHighlighter({ hash: '#L5-25' });
return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything());
});
it('discards click events', function() {
@@ -50,10 +56,10 @@ import '~/line_highlighter';
clickLine(13);
return expect(spy).toHaveBeenPrevented();
});
- return it('handles garbage input from the hash', function() {
+ it('handles garbage input from the hash', function() {
var func;
func = function() {
- return new LineHighlighter('#blob-content-holder');
+ return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' });
};
return expect(func).not.toThrow();
});
diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/javascripts/locale/sprintf_spec.js
new file mode 100644
index 00000000000..52e903b819f
--- /dev/null
+++ b/spec/javascripts/locale/sprintf_spec.js
@@ -0,0 +1,74 @@
+import sprintf from '~/locale/sprintf';
+
+describe('locale', () => {
+ describe('sprintf', () => {
+ it('does not modify string without parameters', () => {
+ const input = 'No parameters';
+
+ const output = sprintf(input);
+
+ expect(output).toBe(input);
+ });
+
+ it('ignores extraneous parameters', () => {
+ const input = 'No parameters';
+
+ const output = sprintf(input, { ignore: 'this' });
+
+ expect(output).toBe(input);
+ });
+
+ it('ignores extraneous placeholders', () => {
+ const input = 'No %{parameters}';
+
+ const output = sprintf(input);
+
+ expect(output).toBe(input);
+ });
+
+ it('replaces parameters', () => {
+ const input = '%{name} has %{count} parameters';
+ const parameters = {
+ name: 'this',
+ count: 2,
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('this has 2 parameters');
+ });
+
+ it('replaces multiple occurrences', () => {
+ const input = 'to %{verb} or not to %{verb}';
+ const parameters = {
+ verb: 'be',
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('to be or not to be');
+ });
+
+ it('escapes parameters', () => {
+ const input = 'contains %{userContent}';
+ const parameters = {
+ userContent: '<script>alert("malicious!")</script>',
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('contains &lt;script&gt;alert(&quot;malicious!&quot;)&lt;/script&gt;');
+ });
+
+ it('does not escape parameters for escapeParameters = false', () => {
+ const input = 'contains %{safeContent}';
+ const parameters = {
+ safeContent: '<strong>bold attempt</strong>',
+ };
+
+ const output = sprintf(input, parameters, false);
+
+ expect(output).toBe('contains <strong>bold attempt</strong>');
+ });
+ });
+});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 6ff42e2378d..3ab901da6b6 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper';
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
+
+ describe('hideCloseButton', () => {
+ describe('merge request of another user', () => {
+ beforeEach(() => {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ this.el = document.querySelector('.merge-request .issuable-actions');
+ const merge = new MergeRequest();
+ merge.hideCloseButton();
+ });
+
+ it('hides the dropdown close item and selects the next item', () => {
+ const closeItem = this.el.querySelector('li.close-item');
+ const smallCloseItem = this.el.querySelector('.js-close-item');
+ const reportItem = this.el.querySelector('li.report-item');
+
+ expect(closeItem).toHaveClass('hidden');
+ expect(smallCloseItem).toHaveClass('hidden');
+ expect(reportItem).toHaveClass('droplab-item-selected');
+ expect(reportItem).not.toHaveClass('hidden');
+ });
+ });
+
+ describe('merge request of current_user', () => {
+ beforeEach(() => {
+ loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
+ this.el = document.querySelector('.merge-request .issuable-actions');
+ const merge = new MergeRequest();
+ merge.hideCloseButton();
+ });
+
+ it('hides the close button', () => {
+ const closeButton = this.el.querySelector('.btn-close');
+ const smallCloseItem = this.el.querySelector('.js-close-item');
+
+ expect(closeButton).toHaveClass('hidden');
+ expect(smallCloseItem).toHaveClass('hidden');
+ });
+ });
+ });
});
}).call(window);
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 8830a2d29e5..ccdbfcba692 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -78,8 +78,9 @@ import 'vendor/jquery.scrollTo';
});
describe('meta click', () => {
+ let metakeyEvent;
beforeEach(function () {
- spyOn(gl.utils, 'isMetaClick').and.returnValue(true);
+ metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
});
it('opens page when commits link is clicked', function () {
@@ -89,7 +90,7 @@ import 'vendor/jquery.scrollTo';
});
this.class.bindEvents();
- document.querySelector('.merge-request-tabs .commits-tab a').click();
+ $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
});
it('opens page when commits badge is clicked', function () {
@@ -99,7 +100,7 @@ import 'vendor/jquery.scrollTo';
});
this.class.bindEvents();
- document.querySelector('.merge-request-tabs .commits-tab a .badge').click();
+ $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
});
});
@@ -415,5 +416,28 @@ import 'vendor/jquery.scrollTo';
});
});
});
+
+ describe('expandViewContainer', function () {
+ beforeEach(() => {
+ $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>');
+ });
+
+ afterEach(() => {
+ $('.content-wrapper').remove();
+ });
+
+ it('removes container-limited from containers', function () {
+ this.class.expandViewContainer();
+
+ expect($('.content-wrapper')).not.toContainElement('.container-limited');
+ });
+
+ it('does remove container-limited from breadcrumbs', function () {
+ $('.container-limited').addClass('breadcrumbs');
+ this.class.expandViewContainer();
+
+ expect($('.content-wrapper')).toContainElement('.container-limited');
+ });
+ });
});
}).call(window);
diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
index e8f7042e131..3319eeb3f31 100644
--- a/spec/javascripts/monitoring/dashboard_state_spec.js
+++ b/spec/javascripts/monitoring/dashboard_state_spec.js
@@ -21,6 +21,9 @@ describe('EmptyState', () => {
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.currentState).toBe(component.states.gettingStarted);
@@ -31,6 +34,9 @@ describe('EmptyState', () => {
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.settingsPath);
@@ -42,6 +48,9 @@ describe('EmptyState', () => {
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.documentationPath);
@@ -53,6 +62,9 @@ describe('EmptyState', () => {
selectedState: 'unableToConnect',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(true);
@@ -63,6 +75,9 @@ describe('EmptyState', () => {
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(false);
@@ -74,6 +89,9 @@ describe('EmptyState', () => {
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -87,6 +105,9 @@ describe('EmptyState', () => {
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -100,6 +121,9 @@ describe('EmptyState', () => {
selectedState: 'unableToConnect',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: 'foo',
+ emptyLoadingSvgPath: 'foo',
+ emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index c2ff38ffab9..dea42d755d4 100644
--- a/spec/javascripts/monitoring/graph/deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -21,6 +21,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
+ graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
@@ -36,6 +37,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -49,6 +51,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -62,6 +65,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -75,6 +79,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
+ graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
@@ -82,12 +87,29 @@ describe('MonitoringDeployment', () => {
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
});
+ it('positions the flag to the left when the xPos is too far right', () => {
+ reducedDeploymentData[0].showDeploymentFlag = false;
+ reducedDeploymentData[0].xPos = 250;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphWidth: 440,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.positionFlag(reducedDeploymentData[0]),
+ ).toBeLessThan(0);
+ });
+
it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -102,6 +124,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -115,6 +138,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -127,6 +151,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 14794cbfd50..8ee1171419d 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -14,19 +14,22 @@ function getCoordinate(component, selector, coordinate) {
return parseInt(coordinateVal, 10);
}
+const defaultValuesComponent = {
+ currentXCoordinate: 200,
+ currentYCoordinate: 100,
+ currentFlagPosition: 100,
+ currentData: {
+ time: new Date('2017-06-04T18:17:33.501Z'),
+ value: '1.49609375',
+ },
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ showFlagContent: true,
+};
+
describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate);
@@ -35,17 +38,7 @@ describe('GraphFlag', () => {
});
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg');
@@ -54,17 +47,7 @@ describe('GraphFlag', () => {
describe('Computed props', () => {
it('calculatedHeight', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
expect(component.calculatedHeight).toEqual(180);
});
diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js
index da2fbd26e23..2571b7ef869 100644
--- a/spec/javascripts/monitoring/graph/legend_spec.js
+++ b/spec/javascripts/monitoring/graph/legend_spec.js
@@ -28,7 +28,7 @@ const defaultValuesComponent = {
currentDataIndex: 0,
};
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result,
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0],
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
@@ -89,13 +89,12 @@ describe('GraphLegend', () => {
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
});
- it('contains text to signal the usage, title and time', () => {
+ it('contains text to signal the usage, title and time with multiple time series', () => {
const component = createComponent(defaultValuesComponent);
const titles = component.$el.querySelectorAll('.legend-metric-title');
- expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1);
- expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1);
- expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1);
+ expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1);
+ expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1);
expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
});
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
new file mode 100644
index 00000000000..81825a3ae87
--- /dev/null
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import GraphPath from '~/monitoring/components/graph/path.vue';
+import createTimeSeries from '~/monitoring/utils/multiple_time_series';
+import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(GraphPath);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
+
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const firstTimeSeries = timeSeries[0];
+
+describe('Monitoring Paths', () => {
+ it('renders two paths to represent a line and the area underneath it', () => {
+ const component = createComponent({
+ generatedLinePath: firstTimeSeries.linePath,
+ generatedAreaPath: firstTimeSeries.areaPath,
+ lineColor: firstTimeSeries.lineColor,
+ areaColor: firstTimeSeries.areaColor,
+ });
+ const metricArea = component.$el.querySelector('.metric-area');
+ const metricLine = component.$el.querySelector('.metric-line');
+
+ expect(metricArea.getAttribute('fill')).toBe('#8fbce8');
+ expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath);
+ expect(metricLine.getAttribute('stroke')).toBe('#1f78d1');
+ expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath);
+ });
+});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 7d8b0744af1..fd79abe241a 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -44,7 +44,7 @@ describe('Graph', () => {
.not.toEqual(-1);
});
- it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
+ it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
graphData: convertedMetrics[1],
classType: 'col-md-6',
@@ -52,8 +52,8 @@ describe('Graph', () => {
deploymentData,
});
- const viewBoxArray = component.outterViewBox.split(' ');
- expect(typeof component.outterViewBox).toEqual('string');
+ const viewBoxArray = component.outerViewBox.split(' ');
+ expect(typeof component.outerViewBox).toEqual('string');
expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
});
@@ -86,4 +86,22 @@ describe('Graph', () => {
expect(component.yAxisLabel).toEqual(component.graphData.y_label);
expect(component.legendTitle).toEqual(component.graphData.queries[0].label);
});
+
+ it('sets the currentData object based on the hovered data index', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ graphIdentifier: 0,
+ hoverData: {
+ hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
+ currentDeployXPos: null,
+ },
+ });
+
+ component.positionFlag();
+ expect(component.currentData).toBe(component.timeSeries[0].values[10]);
+ expect(component.currentDataIndex).toEqual(10);
+ });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 3d399f2bb95..7ceab657464 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -6346,7 +6346,13 @@ export const singleRowMetricsMultipleSeries = [
}
]
},
- ]
+ ],
+ 'when': [
+ {
+ 'value': 'hundred(s)',
+ 'color': 'green',
+ },
+ ],
}
]
},
diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/monitoring_paths_spec.js
deleted file mode 100644
index d39db945e17..00000000000
--- a/spec/javascripts/monitoring/monitoring_paths_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue';
-import createTimeSeries from '~/monitoring/utils/multiple_time_series';
-import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringPaths);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
-
-describe('Monitoring Paths', () => {
- it('renders two paths to represent a line and the area underneath it', () => {
- const component = createComponent({
- generatedLinePath: timeSeries[0].linePath,
- generatedAreaPath: timeSeries[0].areaPath,
- lineColor: '#ccc',
- areaColor: '#fff',
- });
- const metricArea = component.$el.querySelector('.metric-area');
- const metricLine = component.$el.querySelector('.metric-line');
-
- expect(metricArea.getAttribute('fill')).toBe('#fff');
- expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath);
- expect(metricLine.getAttribute('stroke')).toBe('#ccc');
- expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath);
- });
-});
diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
index 3daf6bf82df..7e44a9ade9e 100644
--- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
+++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
@@ -2,16 +2,17 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const firstTimeSeries = timeSeries[0];
describe('Multiple time series', () => {
it('createTimeSeries returned array contains an object for each element', () => {
- expect(typeof timeSeries[0].linePath).toEqual('string');
- expect(typeof timeSeries[0].areaPath).toEqual('string');
- expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function');
- expect(typeof timeSeries[0].areaColor).toEqual('string');
- expect(typeof timeSeries[0].lineColor).toEqual('string');
- expect(timeSeries[0].values instanceof Array).toEqual(true);
+ expect(typeof firstTimeSeries.linePath).toEqual('string');
+ expect(typeof firstTimeSeries.areaPath).toEqual('string');
+ expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function');
+ expect(typeof firstTimeSeries.areaColor).toEqual('string');
+ expect(typeof firstTimeSeries.lineColor).toEqual('string');
+ expect(firstTimeSeries.values instanceof Array).toEqual(true);
});
it('createTimeSeries returns an array', () => {
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index cca5ec887a3..3f659af5c3b 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import autosize from 'vendor/autosize';
import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue';
import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
@@ -32,6 +33,30 @@ describe('issue_comment_form component', () => {
expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
});
+ describe('handleSave', () => {
+ it('should request to save note when note is entered', () => {
+ vm.note = 'hello world';
+ spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
+ spyOn(vm, 'resizeTextarea');
+ spyOn(vm, 'stopPolling');
+
+ vm.handleSave();
+ expect(vm.isSubmitting).toEqual(true);
+ expect(vm.note).toEqual('');
+ expect(vm.saveNote).toHaveBeenCalled();
+ expect(vm.stopPolling).toHaveBeenCalled();
+ expect(vm.resizeTextarea).toHaveBeenCalled();
+ });
+
+ it('should toggle issue state when no note', () => {
+ spyOn(vm, 'toggleIssueState');
+
+ vm.handleSave();
+
+ expect(vm.toggleIssueState).toHaveBeenCalled();
+ });
+ });
+
describe('textarea', () => {
it('should render textarea with placeholder', () => {
expect(
@@ -39,6 +64,22 @@ describe('issue_comment_form component', () => {
).toEqual('Write a comment or drag your files here...');
});
+ it('should make textarea disabled while requesting', (done) => {
+ const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
+ vm.note = 'hello world';
+ spyOn(vm, 'stopPolling');
+ spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
+
+ vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton.
+ $submitButton.trigger('click');
+
+ vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea.
+ expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
+ done();
+ });
+ });
+ });
+
it('should support quick actions', () => {
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
@@ -55,6 +96,19 @@ describe('issue_comment_form component', () => {
expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
});
+ it('should resize textarea after note discarded', (done) => {
+ spyOn(autosize, 'update');
+ spyOn(vm, 'discard').and.callThrough();
+
+ vm.note = 'foo';
+ vm.discard();
+
+ Vue.nextTick(() => {
+ expect(autosize.update).toHaveBeenCalled();
+ done();
+ });
+ });
+
describe('edit mode', () => {
it('should enter edit mode when arrow up is pressed', () => {
spyOn(vm, 'editCurrentUserLastNote').and.callThrough();
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 72d362acb2f..3d1ca870ca4 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,6 +1,5 @@
-
import * as actions from '~/notes/stores/actions';
-import testAction from './helpers';
+import testAction from '../../helpers/vuex_action_helper';
import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index a38f29c1e39..1e22e03e178 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -3,19 +3,31 @@ import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, indiv
describe('Mutation Notes Store', () => {
describe('ADD_NEW_NOTE', () => {
- it('should add a new note to an array of notes', () => {
- const state = { notes: [] };
+ let state;
+ let noteData;
+
+ beforeEach(() => {
+ state = { notes: [] };
+ noteData = {
+ expanded: true,
+ id: note.discussion_id,
+ individual_note: true,
+ notes: [note],
+ reply_id: note.discussion_id,
+ };
mutations.ADD_NEW_NOTE(state, note);
+ });
+ it('should add a new note to an array of notes', () => {
expect(state).toEqual({
- notes: [{
- expanded: true,
- id: note.discussion_id,
- individual_note: true,
- notes: [note],
- reply_id: note.discussion_id,
- }],
+ notes: [noteData],
});
+ expect(state.notes.length).toBe(1);
+ });
+
+ it('should not add the same note to the notes array', () => {
+ mutations.ADD_NEW_NOTE(state, note);
+ expect(state.notes.length).toBe(1);
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 8c5ad8914b0..3e791a31604 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -770,6 +770,20 @@ import '~/notes';
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
+
+ it('should return a escaped user name', () => {
+ const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>';
+ const $tempNote = this.notes.createPlaceholderNote({
+ formContent: sampleComment,
+ uniqueId,
+ isDiscussionNote: false,
+ currentUsername,
+ currentUserFullname: currentUserFullnameXSS,
+ currentUserAvatar,
+ });
+ const $tempNoteHeader = $tempNote.find('.note-header');
+ expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
+ });
});
describe('createPlaceholderSystemNote', () => {
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index bb47a28d9fe..6611b74594f 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -11,6 +11,7 @@ describe('Pipelines Empty State', () => {
component = new EmptyStateComponent({
propsData: {
helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
},
}).$mount();
});
diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js
index f667d351f72..a402857a4d1 100644
--- a/spec/javascripts/pipelines/error_state_spec.js
+++ b/spec/javascripts/pipelines/error_state_spec.js
@@ -8,7 +8,11 @@ describe('Pipelines Error State', () => {
beforeEach(() => {
ErrorStateComponent = Vue.extend(errorStateComp);
- component = new ErrorStateComponent().$mount();
+ component = new ErrorStateComponent({
+ propsData: {
+ errorStateSvgPath: 'foo',
+ },
+ }).$mount();
});
it('should render error state SVG', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 256fdbe743c..4a4f2259d23 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => {
component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
).toEqual('Auto DevOps');
});
+
+ it('should render error badge when pipeline has a failure reason set', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ failure_reason: true,
+ },
+ failure_reason: 'some reason',
+ },
+ autoDevopsHelpPath: 'foo',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-failure').textContent).toContain('error');
+ expect(component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title')).toContain('some reason');
+ });
});
diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
index acb67d0ec21..a8a8e3e2cff 100644
--- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js
+++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
@@ -34,7 +34,7 @@ describe('Pipelines Artifacts dropdown', () => {
).toEqual(artifacts[0].path);
expect(
- component.$el.querySelector('.dropdown-menu li a span').textContent,
+ component.$el.querySelector('.dropdown-menu li a').textContent,
).toContain(artifacts[0].name);
});
});
diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js
index 0a6c479a95b..084ffe08917 100644
--- a/spec/javascripts/pretty_time_spec.js
+++ b/spec/javascripts/pretty_time_spec.js
@@ -1,215 +1,133 @@
-import '~/lib/utils/pretty_time';
+import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time';
-(() => {
- const prettyTime = gl.utils.prettyTime;
+function assertTimeUnits(obj, minutes, hours, days, weeks) {
+ expect(obj.minutes).toBe(minutes);
+ expect(obj.hours).toBe(hours);
+ expect(obj.days).toBe(days);
+ expect(obj.weeks).toBe(weeks);
+}
- describe('prettyTime methods', function () {
- describe('parseSeconds', function () {
- it('should correctly parse a negative value', function () {
- const parser = prettyTime.parseSeconds;
+describe('prettyTime methods', () => {
+ describe('parseSeconds', () => {
+ it('should correctly parse a negative value', () => {
+ const zeroSeconds = parseSeconds(-1000);
- const zeroSeconds = parser(-1000);
-
- expect(zeroSeconds.minutes).toBe(16);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a zero value', function () {
- const parser = prettyTime.parseSeconds;
-
- const zeroSeconds = parser(0);
-
- expect(zeroSeconds.minutes).toBe(0);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a small non-zero second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const subOneMinute = parser(10);
-
- expect(subOneMinute.minutes).toBe(0);
- expect(subOneMinute.hours).toBe(0);
- expect(subOneMinute.days).toBe(0);
- expect(subOneMinute.weeks).toBe(0);
-
- const aboveOneMinute = parser(100);
-
- expect(aboveOneMinute.minutes).toBe(1);
- expect(aboveOneMinute.hours).toBe(0);
- expect(aboveOneMinute.days).toBe(0);
- expect(aboveOneMinute.weeks).toBe(0);
-
- const manyMinutes = parser(1000);
-
- expect(manyMinutes.minutes).toBe(16);
- expect(manyMinutes.hours).toBe(0);
- expect(manyMinutes.days).toBe(0);
- expect(manyMinutes.weeks).toBe(0);
- });
-
- it('should correctly parse large second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const aboveOneHour = parser(4800);
-
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
-
- const aboveOneDay = parser(110000);
-
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(3);
- expect(aboveOneDay.weeks).toBe(0);
-
- const aboveOneWeek = parser(25000000);
-
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(3);
- expect(aboveOneWeek.weeks).toBe(173);
- });
+ assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
+ });
- it('should correctly accept a custom param for hoursPerDay', function () {
- const parser = prettyTime.parseSeconds;
- const config = { hoursPerDay: 24 };
+ it('should correctly parse a zero value', () => {
+ const zeroSeconds = parseSeconds(0);
- const aboveOneHour = parser(4800, config);
+ assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
+ });
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ it('should correctly parse a small non-zero second values', () => {
+ const subOneMinute = parseSeconds(10);
+ const aboveOneMinute = parseSeconds(100);
+ const manyMinutes = parseSeconds(1000);
- const aboveOneDay = parser(110000, config);
+ assertTimeUnits(subOneMinute, 0, 0, 0, 0);
+ assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
+ assertTimeUnits(manyMinutes, 16, 0, 0, 0);
+ });
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(1);
- expect(aboveOneDay.weeks).toBe(0);
+ it('should correctly parse large second values', () => {
+ const aboveOneHour = parseSeconds(4800);
+ const aboveOneDay = parseSeconds(110000);
+ const aboveOneWeek = parseSeconds(25000000);
- const aboveOneWeek = parser(25000000, config);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
+ });
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(8);
- expect(aboveOneWeek.days).toBe(4);
+ it('should correctly accept a custom param for hoursPerDay', () => {
+ const config = { hoursPerDay: 24 };
- expect(aboveOneWeek.weeks).toBe(57);
- });
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- it('should correctly accept a custom param for daysPerWeek', function () {
- const parser = prettyTime.parseSeconds;
- const config = { daysPerWeek: 7 };
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
+ assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
+ });
- const aboveOneHour = parser(4800, config);
+ it('should correctly accept a custom param for daysPerWeek', () => {
+ const config = { daysPerWeek: 7 };
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- const aboveOneDay = parser(110000, config);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
+ });
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(3);
- expect(aboveOneDay.weeks).toBe(0);
+ it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
+ const config = { daysPerWeek: 55, hoursPerDay: 14 };
- const aboveOneWeek = parser(25000000, config);
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(0);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
+ });
+ });
- expect(aboveOneWeek.weeks).toBe(124);
- });
+ describe('stringifyTime', () => {
+ it('should stringify values with all non-zero units', () => {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
- it('should correctly accept custom params for daysPerWeek and hoursPerDay', function () {
- const parser = prettyTime.parseSeconds;
- const config = { daysPerWeek: 55, hoursPerDay: 14 };
+ const timeString = stringifyTime(timeObject);
- const aboveOneHour = parser(4800, config);
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ it('should stringify values with some non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
- const aboveOneDay = parser(110000, config);
+ const timeString = stringifyTime(timeObject);
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(2);
- expect(aboveOneDay.days).toBe(2);
- expect(aboveOneDay.weeks).toBe(0);
+ expect(timeString).toBe('4d 20m');
+ });
- const aboveOneWeek = parser(25000000, config);
+ it('should stringify values with no non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(1);
+ const timeString = stringifyTime(timeObject);
- expect(aboveOneWeek.weeks).toBe(9);
- });
+ expect(timeString).toBe('0m');
});
+ });
- describe('stringifyTime', function () {
- it('should stringify values with all non-zero units', function () {
- const timeObject = {
- weeks: 1,
- days: 4,
- hours: 7,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('1w 4d 7h 20m');
- });
-
- it('should stringify values with some non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 4,
- hours: 0,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('4d 20m');
- });
-
- it('should stringify values with no non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 0,
- hours: 0,
- minutes: 0,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('0m');
- });
+ describe('abbreviateTime', () => {
+ it('should abbreviate stringified times for weeks', () => {
+ const fullTimeString = '1w 3d 4h 5m';
+ expect(abbreviateTime(fullTimeString)).toBe('1w');
});
- describe('abbreviateTime', function () {
- it('should abbreviate stringified times for weeks', function () {
- const fullTimeString = '1w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', function () {
- const fullTimeString = '0w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
- });
+ it('should abbreviate stringified times for non-weeks', () => {
+ const fullTimeString = '0w 3d 4h 5m';
+ expect(abbreviateTime(fullTimeString)).toBe('3d');
});
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
new file mode 100644
index 00000000000..2e94948cfb2
--- /dev/null
+++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+
+import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
+
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('DeleteAccountModal component', () => {
+ const actionUrl = `${gl.TEST_HOST}/delete/user`;
+ const username = 'hasnoname';
+ let Component;
+ let vm;
+
+ beforeEach(() => {
+ Component = Vue.extend(deleteAccountModal);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const findElements = () => {
+ const confirmation = vm.confirmWithPassword ? 'password' : 'username';
+ return {
+ form: vm.$refs.form,
+ input: vm.$el.querySelector(`[name="${confirmation}"]`),
+ submitButton: vm.$el.querySelector('.btn-danger'),
+ };
+ };
+
+ describe('with password confirmation', () => {
+ beforeEach((done) => {
+ vm = mountComponent(Component, {
+ actionUrl,
+ confirmWithPassword: true,
+ username,
+ });
+
+ vm.isOpen = true;
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not accept empty password', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(submitButton).toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('submits form with password', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = 'anything';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(submitButton).not.toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('with username confirmation', () => {
+ beforeEach((done) => {
+ vm = mountComponent(Component, {
+ actionUrl,
+ confirmWithPassword: false,
+ username,
+ });
+
+ vm.isOpen = true;
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not accept wrong username', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = 'this is wrong';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(submitButton).toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('submits form with correct username', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = username;
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(submitButton).not.toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
index d5dd8b3449a..cfd1bb7d24f 100644
--- a/spec/javascripts/projects_dropdown/service/projects_service_spec.js
+++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
@@ -34,7 +34,7 @@ describe('ProjectsService', () => {
const searchQuery = 'lab';
const queryParams = {
- simple: false,
+ simple: true,
per_page: 20,
membership: true,
order_by: 'last_activity_at',
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
new file mode 100644
index 00000000000..43e7d9e1224
--- /dev/null
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import registry from '~/registry/components/app.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { reposServerResponse } from '../mock_data';
+
+describe('Registry List', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(registry);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render a list of repos', (done) => {
+ setTimeout(() => {
+ expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.container-image').length,
+ ).toEqual(reposServerResponse.length);
+ done();
+ });
+ }, 0);
+ });
+
+ describe('delete repository', () => {
+ it('should be possible to delete a repo', (done) => {
+ setTimeout(() => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
+ done();
+ });
+ }, 0);
+ });
+ });
+
+ describe('toggle repository', () => {
+ it('should open the container', (done) => {
+ setTimeout(() => {
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up');
+ done();
+ });
+ });
+ }, 0);
+ });
+ });
+ });
+
+ describe('without data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render empty message', (done) => {
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('p').textContent.trim(),
+ ).toEqual('No container images stored for this project. Add one by following the instructions above.');
+ done();
+ }, 0);
+ });
+ });
+
+ describe('while loading data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render a loading spinner', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js
new file mode 100644
index 00000000000..5891921318a
--- /dev/null
+++ b/spec/javascripts/registry/components/collapsible_container_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import collapsibleComponent from '~/registry/components/collapsible_container.vue';
+import store from '~/registry/stores';
+import { repoPropsData } from '../mock_data';
+
+describe('collapsible registry container', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(collapsibleComponent);
+ vm = new Component({
+ store,
+ propsData: {
+ repo: repoPropsData,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggle', () => {
+ it('should be closed by default', () => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
+ });
+
+ it('should be open when user clicks on closed repo', (done) => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up');
+ done();
+ });
+ });
+
+ it('should be closed when the user clicks on an opened repo', (done) => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('delete repo', () => {
+ it('should be possible to delete a repo', () => {
+ expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js
new file mode 100644
index 00000000000..6aa61afc445
--- /dev/null
+++ b/spec/javascripts/registry/components/table_registry_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import tableRegistry from '~/registry/components/table_registry.vue';
+import store from '~/registry/stores';
+import { repoPropsData } from '../mock_data';
+
+describe('table registry', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(tableRegistry);
+ vm = new Component({
+ store,
+ propsData: {
+ repo: repoPropsData,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a table with the registry list', () => {
+ expect(
+ vm.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(repoPropsData.list.length);
+ });
+
+ it('should render registry tag', () => {
+ const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' ');
+ expect(textRendered).toContain(repoPropsData.list[0].tag);
+ expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
+ expect(textRendered).toContain(repoPropsData.list[0].layers);
+ expect(textRendered).toContain(repoPropsData.list[0].size);
+ });
+
+ it('should be possible to delete a registry', () => {
+ expect(
+ vm.$el.querySelector('.table tbody tr .js-delete-registry'),
+ ).toBeDefined();
+ });
+
+ describe('pagination', () => {
+ it('should be possible to change the page', () => {
+ expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js
new file mode 100644
index 00000000000..3d989541881
--- /dev/null
+++ b/spec/javascripts/registry/getters_spec.js
@@ -0,0 +1,43 @@
+import * as getters from '~/registry/stores/getters';
+
+describe('Getters Registry Store', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ isLoading: false,
+ endpoint: '/root/empty-project/container_registry.json',
+ repos: [{
+ canDelete: true,
+ destroyPath: 'bar',
+ id: '134',
+ isLoading: false,
+ list: [],
+ location: 'foo',
+ name: 'gitlab-org/omnibus-gitlab/foo',
+ tagsPath: 'foo',
+ }, {
+ canDelete: true,
+ destroyPath: 'bar',
+ id: '123',
+ isLoading: false,
+ list: [],
+ location: 'foo',
+ name: 'gitlab-org/omnibus-gitlab',
+ tagsPath: 'foo',
+ }],
+ };
+ });
+
+ describe('isLoading', () => {
+ it('should return the isLoading property', () => {
+ expect(getters.isLoading(state)).toEqual(state.isLoading);
+ });
+ });
+
+ describe('repos', () => {
+ it('should return the repos', () => {
+ expect(getters.repos(state)).toEqual(state.repos);
+ });
+ });
+});
diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js
new file mode 100644
index 00000000000..18600d00bff
--- /dev/null
+++ b/spec/javascripts/registry/mock_data.js
@@ -0,0 +1,122 @@
+export const defaultState = {
+ isLoading: false,
+ endpoint: '',
+ repos: [],
+};
+
+export const reposServerResponse = [
+ {
+ destroy_path: 'path',
+ id: '123',
+ location: 'location',
+ path: 'foo',
+ tags_path: 'tags_path',
+ },
+ {
+ destroy_path: 'path_',
+ id: '456',
+ location: 'location_',
+ path: 'bar',
+ tags_path: 'tags_path_',
+ },
+];
+
+export const registryServerResponse = [
+ {
+ name: 'centos7',
+ short_revision: 'b118ab5b0',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ size: 679,
+ layers: 19,
+ location: 'location',
+ created_at: 1505828744434,
+ destroy_path: 'path_',
+ },
+ {
+ name: 'centos6',
+ short_revision: 'b118ab5b0',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ size: 679,
+ layers: 19,
+ location: 'location',
+ created_at: 1505828744434,
+ }];
+
+export const parsedReposServerResponse = [
+ {
+ canDelete: true,
+ destroyPath: reposServerResponse[0].destroy_path,
+ id: reposServerResponse[0].id,
+ isLoading: false,
+ list: [],
+ location: reposServerResponse[0].location,
+ name: reposServerResponse[0].path,
+ tagsPath: reposServerResponse[0].tags_path,
+ },
+ {
+ canDelete: true,
+ destroyPath: reposServerResponse[1].destroy_path,
+ id: reposServerResponse[1].id,
+ isLoading: false,
+ list: [],
+ location: reposServerResponse[1].location,
+ name: reposServerResponse[1].path,
+ tagsPath: reposServerResponse[1].tags_path,
+ },
+];
+
+export const parsedRegistryServerResponse = [
+ {
+ tag: registryServerResponse[0].name,
+ revision: registryServerResponse[0].revision,
+ shortRevision: registryServerResponse[0].short_revision,
+ size: registryServerResponse[0].size,
+ layers: registryServerResponse[0].layers,
+ location: registryServerResponse[0].location,
+ createdAt: registryServerResponse[0].created_at,
+ destroyPath: registryServerResponse[0].destroy_path,
+ canDelete: true,
+ },
+ {
+ tag: registryServerResponse[1].name,
+ revision: registryServerResponse[1].revision,
+ shortRevision: registryServerResponse[1].short_revision,
+ size: registryServerResponse[1].size,
+ layers: registryServerResponse[1].layers,
+ location: registryServerResponse[1].location,
+ createdAt: registryServerResponse[1].created_at,
+ destroyPath: registryServerResponse[1].destroy_path,
+ canDelete: false,
+ },
+];
+
+export const repoPropsData = {
+ canDelete: true,
+ destroyPath: 'path',
+ id: '123',
+ isLoading: false,
+ list: [
+ {
+ tag: 'centos6',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ shortRevision: 'b118ab5b0',
+ size: 19,
+ layers: 10,
+ location: 'location',
+ createdAt: 1505828744434,
+ destroyPath: 'path',
+ canDelete: true,
+ },
+ ],
+ location: 'location',
+ name: 'foo',
+ tagsPath: 'path',
+ pagination: {
+ perPage: 5,
+ page: 1,
+ total: 13,
+ totalPages: 1,
+ nextPage: null,
+ previousPage: null,
+ },
+};
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
new file mode 100644
index 00000000000..3c9da4f107b
--- /dev/null
+++ b/spec/javascripts/registry/stores/actions_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import _ from 'underscore';
+import * as actions from '~/registry/stores/actions';
+import * as types from '~/registry/stores/mutation_types';
+import testAction from '../../helpers/vuex_action_helper';
+import {
+ defaultState,
+ reposServerResponse,
+ registryServerResponse,
+ parsedReposServerResponse,
+} from '../mock_data';
+
+Vue.use(VueResource);
+
+describe('Actions Registry Store', () => {
+ let interceptor;
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = defaultState;
+ });
+
+ describe('server requests', () => {
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ describe('fetchRepos', () => {
+ beforeEach(() => {
+ interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ it('should set receveived repos', (done) => {
+ testAction(actions.fetchRepos, null, mockedState, [
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.SET_REPOS_LIST, payload: reposServerResponse },
+ ], done);
+ });
+ });
+
+ describe('fetchList', () => {
+ beforeEach(() => {
+ interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(registryServerResponse), {
+ status: 200,
+ }));
+ };
+
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ it('should set received list', (done) => {
+ mockedState.repos = parsedReposServerResponse;
+
+ testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING },
+ { type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
+ ], done);
+ });
+ });
+ });
+
+ describe('setMainEndpoint', () => {
+ it('should commit set main endpoint', (done) => {
+ testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
+ { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
+ ], done);
+ });
+ });
+
+ describe('toggleLoading', () => {
+ it('should commit toggle main loading', (done) => {
+ testAction(actions.toggleLoading, null, mockedState, [
+ { type: types.TOGGLE_MAIN_LOADING },
+ ], done);
+ });
+ });
+});
diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js
new file mode 100644
index 00000000000..2e4c0659daa
--- /dev/null
+++ b/spec/javascripts/registry/stores/mutations_spec.js
@@ -0,0 +1,81 @@
+import mutations from '~/registry/stores/mutations';
+import * as types from '~/registry/stores/mutation_types';
+import {
+ defaultState,
+ reposServerResponse,
+ registryServerResponse,
+ parsedReposServerResponse,
+ parsedRegistryServerResponse,
+} from '../mock_data';
+
+describe('Mutations Registry Store', () => {
+ let mockState;
+ beforeEach(() => {
+ mockState = defaultState;
+ });
+
+ describe('SET_MAIN_ENDPOINT', () => {
+ it('should set the main endpoint', () => {
+ const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
+ expect(mockState).toEqual(expectedState);
+ });
+ });
+
+ describe('SET_REPOS_LIST', () => {
+ it('should set a parsed repository list', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ expect(mockState.repos).toEqual(parsedReposServerResponse);
+ });
+ });
+
+ describe('TOGGLE_MAIN_LOADING', () => {
+ it('should set a parsed repository list', () => {
+ mutations[types.TOGGLE_MAIN_LOADING](mockState);
+ expect(mockState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('SET_REGISTRY_LIST', () => {
+ it('should set a list of registries in a specific repository', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ mutations[types.SET_REGISTRY_LIST](mockState, {
+ repo: mockState.repos[0],
+ resp: registryServerResponse,
+ headers: {
+ 'x-per-page': 2,
+ 'x-page': 1,
+ 'x-total': 10,
+ },
+ });
+
+ expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse);
+ expect(mockState.repos[0].pagination).toEqual({
+ perPage: 2,
+ page: 1,
+ total: 10,
+ totalPages: NaN,
+ nextPage: NaN,
+ previousPage: NaN,
+ });
+ });
+ });
+
+ describe('TOGGLE_REGISTRY_LIST_LOADING', () => {
+ it('should toggle isLoading property for a specific repository', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ mutations[types.SET_REGISTRY_LIST](mockState, {
+ repo: mockState.repos[0],
+ resp: registryServerResponse,
+ headers: {
+ 'x-per-page': 2,
+ 'x-page': 1,
+ 'x-total': 10,
+ },
+ });
+
+ mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]);
+ expect(mockState.repos[0].isLoading).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index 29dc2d21e4b..411514009dc 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -21,13 +21,11 @@ describe('RepoEditButton', () => {
expect(vm.$el.textContent).toMatch('Edit');
spyOn(vm, 'editCancelClicked').and.callThrough();
- spyOn(vm, 'toggleProjectRefsForm');
vm.$el.click();
Vue.nextTick(() => {
expect(vm.editCancelClicked).toHaveBeenCalled();
- expect(vm.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index 518a2d25ecf..620b604f404 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import repoFile from '~/repo/components/repo_file.vue';
+import RepoStore from '~/repo/stores/repo_store';
describe('RepoFile', () => {
const updated = 'updated';
@@ -12,8 +13,13 @@ describe('RepoFile', () => {
level: 10,
};
const activeFile = {
+ pageTitle: 'pageTitle',
url: 'url',
};
+ const otherFile = {
+ html: '<p class="file-content">html</p>',
+ pageTitle: 'otherpageTitle',
+ };
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
@@ -23,15 +29,17 @@ describe('RepoFile', () => {
}).$mount();
}
- beforeEach(() => {
- spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated);
- });
-
it('renders link, icon, name and last commit details', () => {
- const vm = createComponent({
- file,
- activeFile,
+ const RepoFile = Vue.extend(repoFile);
+ const vm = new RepoFile({
+ propsData: {
+ file,
+ activeFile,
+ },
});
+ spyOn(vm, 'timeFormated').and.returnValue(updated);
+ vm.$mount();
+
const name = vm.$el.querySelector('.repo-file-name');
const fileIcon = vm.$el.querySelector('.file-icon');
@@ -60,6 +68,12 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
+ it('sets the document title correctly', () => {
+ RepoStore.setActiveFiles(otherFile);
+
+ expect(document.title.trim()).toEqual(otherFile.pageTitle);
+ });
+
it('renders a spinner if the file is loading', () => {
file.loading = true;
const vm = createComponent({
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index abcff8e537e..35d2b37ac2a 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -5,26 +5,34 @@ import RepoStore from '~/repo/stores/repo_store';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
describe('RepoSidebar', () => {
+ let vm;
+
function createComponent() {
const RepoSidebar = Vue.extend(repoSidebar);
return new RepoSidebar().$mount();
}
+ afterEach(() => {
+ vm.$destroy();
+ });
+
it('renders a sidebar', () => {
RepoStore.files = [{
id: 0,
}];
RepoStore.openedFiles = [];
- const vm = createComponent();
+ RepoStore.isRoot = false;
+
+ vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(thead.querySelector('.name').textContent).toEqual('Name');
- expect(thead.querySelector('.last-commit').textContent).toEqual('Last Commit');
- expect(thead.querySelector('.last-update').textContent).toEqual('Last Update');
+ expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit');
+ expect(thead.querySelector('.last-update').textContent).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
@@ -35,7 +43,7 @@ describe('RepoSidebar', () => {
RepoStore.openedFiles = [{
id: 0,
}];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
expect(vm.$el.querySelector('thead')).toBeFalsy();
@@ -47,7 +55,7 @@ describe('RepoSidebar', () => {
tree: true,
};
RepoStore.files = [];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
});
@@ -57,7 +65,7 @@ describe('RepoSidebar', () => {
id: 0,
}];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
});
@@ -72,13 +80,27 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
expect(Helper.getContent).toHaveBeenCalledWith(file1);
});
+ it('should not fetch data for already opened files', () => {
+ const file = {
+ id: 42,
+ url: 'foo',
+ };
+
+ spyOn(Helper, 'getFileFromPath').and.returnValue(file);
+ spyOn(RepoStore, 'setActiveFiles');
+ vm = createComponent();
+ vm.fileClicked(file);
+
+ expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file);
+ });
+
it('should hide files in directory if already open', () => {
spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough();
const file1 = {
@@ -89,7 +111,7 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
@@ -100,12 +122,48 @@ describe('RepoSidebar', () => {
describe('goToPreviousDirectoryClicked', () => {
it('should hide files in directory if already open', () => {
const prevUrl = 'foo/bar';
- const vm = createComponent();
+ vm = createComponent();
vm.goToPreviousDirectoryClicked(prevUrl);
expect(RepoService.url).toEqual(prevUrl);
});
});
+
+ describe('back button', () => {
+ const file1 = {
+ id: 1,
+ url: 'file1',
+ };
+ const file2 = {
+ id: 2,
+ url: 'file2',
+ };
+ RepoStore.files = [file1, file2];
+ RepoStore.openedFiles = [file1, file2];
+ RepoStore.isRoot = true;
+
+ vm = createComponent();
+ vm.fileClicked(file1);
+
+ it('render previous file when using back button', () => {
+ spyOn(Helper, 'getContent').and.callThrough();
+
+ vm.fileClicked(file2);
+ expect(Helper.getContent).toHaveBeenCalledWith(file2);
+ Helper.getContent.calls.reset();
+
+ history.pushState({
+ key: Math.random(),
+ }, '', file1.url);
+ const popEvent = document.createEvent('Event');
+ popEvent.initEvent('popstate', true, true);
+ window.dispatchEvent(popEvent);
+
+ expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url);
+
+ window.history.pushState({}, null, '/');
+ });
+ });
});
});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index f2072a6f350..5505f983d71 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -32,56 +32,86 @@ import '~/right_sidebar';
};
describe('RightSidebar', function() {
- var fixtureName = 'issues/open-issue.html.raw';
- preloadFixtures(fixtureName);
- loadJSONFixtures('todos/todos.json');
-
- beforeEach(function() {
- loadFixtures(fixtureName);
- this.sidebar = new Sidebar;
- $aside = $('.right-sidebar');
- $page = $('.page-with-sidebar');
- $icon = $aside.find('i');
- $toggle = $aside.find('.js-sidebar-toggle');
- return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
- });
- it('should expand/collapse the sidebar when arrow is clicked', function() {
- assertSidebarState('expanded');
- $toggle.click();
- assertSidebarState('collapsed');
- $toggle.click();
- assertSidebarState('expanded');
- });
- it('should float over the page and when sidebar icons clicked', function() {
- $labelsIcon.click();
- return assertSidebarState('expanded');
- });
- it('should collapse when the icon arrow clicked while it is floating on page', function() {
- $labelsIcon.click();
- assertSidebarState('expanded');
- $toggle.click();
- return assertSidebarState('collapsed');
+ describe('fixture tests', () => {
+ var fixtureName = 'issues/open-issue.html.raw';
+ preloadFixtures(fixtureName);
+ loadJSONFixtures('todos/todos.json');
+
+ beforeEach(function() {
+ loadFixtures(fixtureName);
+ this.sidebar = new Sidebar;
+ $aside = $('.right-sidebar');
+ $page = $('.page-with-sidebar');
+ $icon = $aside.find('i');
+ $toggle = $aside.find('.js-sidebar-toggle');
+ return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
+ });
+ it('should expand/collapse the sidebar when arrow is clicked', function() {
+ assertSidebarState('expanded');
+ $toggle.click();
+ assertSidebarState('collapsed');
+ $toggle.click();
+ assertSidebarState('expanded');
+ });
+ it('should float over the page and when sidebar icons clicked', function() {
+ $labelsIcon.click();
+ return assertSidebarState('expanded');
+ });
+ it('should collapse when the icon arrow clicked while it is floating on page', function() {
+ $labelsIcon.click();
+ assertSidebarState('expanded');
+ $toggle.click();
+ return assertSidebarState('collapsed');
+ });
+
+ it('should broadcast todo:toggle event when add todo clicked', function() {
+ var todos = getJSONFixture('todos/todos.json');
+ spyOn(jQuery, 'ajax').and.callFake(function() {
+ var d = $.Deferred();
+ var response = todos;
+ d.resolve(response);
+ return d.promise();
+ });
+
+ var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
+
+ $('.issuable-sidebar-header .js-issuable-todo').click();
+
+ expect(todoToggleSpy.calls.count()).toEqual(1);
+ });
+
+ it('should not hide collapsed icons', () => {
+ [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
+ expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
+ });
+ });
});
- it('should broadcast todo:toggle event when add todo clicked', function() {
- var todos = getJSONFixture('todos/todos.json');
- spyOn(jQuery, 'ajax').and.callFake(function() {
- var d = $.Deferred();
- var response = todos;
- d.resolve(response);
- return d.promise();
+ describe('sidebarToggleClicked', () => {
+ const event = jasmine.createSpyObj('event', ['preventDefault']);
+
+ beforeEach(() => {
+ spyOn($.fn, 'hasClass').and.returnValue(false);
+ });
+
+ afterEach(() => {
+ gl.lazyLoader = undefined;
});
- var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
+ it('calls loadCheck if lazyLoader is set', () => {
+ gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
- $('.issuable-sidebar-header .js-issuable-todo').click();
+ Sidebar.prototype.sidebarToggleClicked(event);
- expect(todoToggleSpy.calls.count()).toEqual(1);
- });
+ expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
+ });
+
+ it('does not throw if lazyLoader is not defined', () => {
+ gl.lazyLoader = undefined;
+
+ const toggle = Sidebar.prototype.sidebarToggleClicked.bind(null, event);
- it('should not hide collapsed icons', () => {
- [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
- expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
+ expect(toggle).not.toThrow();
});
});
});
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..b0ea8ae0206
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('EditFormButtons', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editFormButtons);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = mountComponent(Component, {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ });
+
+ vm2 = mountComponent(Component, {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ });
+ });
+
+ it('renders unlock or lock text based on locked state', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/javascripts/sidebar/lock/edit_form_spec.js
new file mode 100644
index 00000000000..7abd6997a18
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import editForm from '~/sidebar/components/lock/edit_form.vue';
+
+describe('EditForm', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editForm);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('renders on the appropriate warning text', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock this issue?'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock this merge request?'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
new file mode 100644
index 00000000000..696fca516bc
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -0,0 +1,71 @@
+import Vue from 'vue';
+import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+
+describe('LockIssueSidebar', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(lockIssueSidebar);
+
+ const mediator = {
+ service: {
+ update: Promise.resolve(true),
+ },
+
+ store: {
+ isLockDialogOpen: false,
+ },
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ isEditable: true,
+ mediator,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ isEditable: false,
+ mediator,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('shows if locked and/or editable', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Edit'),
+ ).toBe(true);
+
+ expect(
+ vm1.$el.innerHTML.includes('Locked'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Unlocked'),
+ ).toBe(true);
+ });
+
+ it('displays the edit form when editable', (done) => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ vm1.$nextTick(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('Unlock this issue?'),
+ ).toBe(true);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index fd492159081..7d3c9319a11 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -26,37 +26,30 @@ describe('Todos', () => {
describe('meta click', () => {
let visitUrlSpy;
+ let windowOpenSpy;
+ let metakeyEvent;
beforeEach(() => {
- spyOn(gl.utils, 'isMetaClick').and.returnValue(true);
+ metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {});
+ windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
- it('opens the todo url in another tab', (done) => {
+ it('opens the todo url in another tab', () => {
const todoLink = todoItem.dataset.url;
- spyOn(window, 'open').and.callFake((url, target) => {
- expect(todoLink).toEqual(url);
- expect(target).toEqual('_blank');
- done();
- });
+ $('.todos-list .todo').trigger(metakeyEvent);
- todoItem.click();
expect(visitUrlSpy).not.toHaveBeenCalled();
+ expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank');
});
- it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => {
- const avatarImage = todoItem.querySelector('img');
- const avatarUrl = avatarImage.parentElement.getAttribute('href');
+ it('run native funcionality when avatar is clicked', () => {
+ $('.todos-list a').on('click', e => e.preventDefault());
+ $('.todos-list img').trigger(metakeyEvent);
- spyOn(window, 'open').and.callFake((url, target) => {
- expect(avatarUrl).toEqual(url);
- expect(target).toEqual('_blank');
- done();
- });
-
- avatarImage.click();
expect(visitUrlSpy).not.toHaveBeenCalled();
+ expect(windowOpenSpy).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
deleted file mode 100644
index 69cb93bd850..00000000000
--- a/spec/javascripts/user_callout_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Cookies from 'js-cookie';
-import UserCallout from '~/user_callout';
-
-const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
-
-describe('UserCallout', function () {
- const fixtureName = 'dashboard/user-callout.html.raw';
- preloadFixtures(fixtureName);
-
- beforeEach(() => {
- loadFixtures(fixtureName);
- Cookies.remove(USER_CALLOUT_COOKIE);
-
- this.userCallout = new UserCallout();
- this.closeButton = $('.js-close-callout.close');
- this.userCalloutBtn = $('.js-close-callout:not(.close)');
- });
-
- it('hides when user clicks on the dismiss-icon', (done) => {
- this.closeButton.click();
- expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
-
- setTimeout(() => {
- expect(
- document.querySelector('.user-callout'),
- ).toBeNull();
-
- done();
- });
- });
-
- it('hides when user clicks on the "check it out" button', () => {
- this.userCalloutBtn.click();
- expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
- });
-
- describe('Sets cookie with setCalloutPerProject', () => {
- beforeEach(() => {
- spyOn(Cookies, 'set').and.callFake(() => {});
- document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar');
- this.userCallout = new UserCallout({ setCalloutPerProject: true });
- });
-
- it('sets a cookie when the user clicks the close button', () => {
- this.userCalloutBtn.click();
- expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' }));
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index c763487d12f..690665ae12c 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -37,6 +37,26 @@ describe('MRWidgetPipeline', () => {
});
});
+ describe('hasPipeline', () => {
+ it('should return true when there is a pipeline', () => {
+ expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0);
+
+ const vm = createComponent({
+ pipeline: mockData.pipeline,
+ });
+
+ expect(vm.hasPipeline).toBeTruthy();
+ });
+
+ it('should return false when there is no pipeline', () => {
+ const vm = createComponent({
+ pipeline: null,
+ });
+
+ expect(vm.hasPipeline).toBeFalsy();
+ });
+ });
+
describe('hasCIError', () => {
it('should return false when there is no CI error', () => {
const vm = createComponent({
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 47303d1e80f..d23b558f4ea 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -4,11 +4,15 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid
const mr = {
targetBranch: 'good-branch',
targetBranchPath: '/good-branch',
- closedBy: {
- name: 'Fatih Acet',
- username: 'fatihacet',
+ closedEvent: {
+ author: {
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ },
+ updatedAt: 'closedEventUpdatedAt',
+ formattedUpdatedAt: '',
},
- updatedAt: '2017-03-23T20:08:08.845Z',
+ updatedAt: 'mrUpdatedAt',
closedAt: '1 day ago',
};
@@ -18,7 +22,7 @@ const createComponent = () => {
return new Component({
el: document.createElement('div'),
propsData: { mr },
- }).$el;
+ });
};
describe('MRWidgetClosed', () => {
@@ -38,14 +42,30 @@ describe('MRWidgetClosed', () => {
});
describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent();
+ let vm;
+ let el;
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should have correct elements', () => {
expect(el.querySelector('h4').textContent).toContain('Closed by');
- expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
+ expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name);
expect(el.textContent).toContain('The changes were not merged into');
expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
});
+
+ it('should use closedEvent updatedAt as tooltip title', () => {
+ expect(
+ el.querySelector('time').getAttribute('title'),
+ ).toBe('closedEventUpdatedAt');
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 3b7b7d93662..5d4c7ec09dc 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,20 +1,9 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+const ConflictsComponent = Vue.extend(conflictsComponent);
const path = '/conflicts';
-const createComponent = () => {
- const Component = Vue.extend(conflictsComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: {
- mr: {
- canMerge: true,
- conflictResolutionPath: path,
- },
- },
- });
-};
describe('MRWidgetConflicts', () => {
describe('props', () => {
@@ -27,44 +16,90 @@ describe('MRWidgetConflicts', () => {
});
describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent().$el;
- const resolveButton = el.querySelector('.js-resolve-conflicts-button');
- const mergeButton = el.querySelector('.mr-widget-body .btn');
- const mergeLocallyButton = el.querySelector('.js-merge-locally-button');
-
- expect(el.textContent).toContain('There are merge conflicts');
- expect(el.textContent).not.toContain('ask someone with write access');
- expect(el.querySelector('.btn-success').disabled).toBeTruthy();
- expect(resolveButton.textContent).toContain('Resolve conflicts');
- expect(resolveButton.getAttribute('href')).toEqual(path);
- expect(mergeButton.textContent).toContain('Merge');
- expect(mergeLocallyButton.textContent).toContain('Merge locally');
+ describe('when allowed to merge', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(ConflictsComponent, {
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: path,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(vm.$el.textContent).toContain('There are merge conflicts');
+ expect(vm.$el.textContent).not.toContain('ask someone with write access');
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button');
+
+ expect(resolveButton.textContent).toContain('Resolve conflicts');
+ expect(resolveButton.getAttribute('href')).toEqual(path);
+ });
+
+ it('should have merge buttons', () => {
+ const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
+ const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
+
+ expect(mergeButton.textContent).toContain('Merge');
+ expect(mergeButton.disabled).toBeTruthy();
+ expect(mergeButton.classList.contains('btn-success')).toEqual(true);
+ expect(mergeLocallyButton.textContent).toContain('Merge locally');
+ });
});
describe('when user does not have permission to merge', () => {
let vm;
beforeEach(() => {
- vm = createComponent();
- vm.mr.canMerge = false;
+ vm = mountComponent(ConflictsComponent, {
+ mr: {
+ canMerge: false,
+ },
+ });
});
- it('should show proper message', (done) => {
- Vue.nextTick(() => {
- expect(vm.$el.textContent).toContain('ask someone with write access');
- done();
- });
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should show proper message', () => {
+ expect(vm.$el.textContent).toContain('ask someone with write access');
+ });
+
+ it('should not have action buttons', () => {
+ expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
+ expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
+ expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
});
+ });
- it('should not have action buttons', (done) => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
- expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null);
- expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null);
- done();
+ describe('when fast-forward or semi-linear merge enabled', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(ConflictsComponent, {
+ mr: {
+ shouldBeRebased: true,
+ },
});
});
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should tell you to rebase locally', () => {
+ expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.');
+ expect(vm.$el.textContent).toContain('To merge this request, first rebase locally');
+ });
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index afaa750199a..2714e8294fa 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -14,9 +14,12 @@ const createComponent = () => {
canRevertInCurrentMR: true,
canRemoveSourceBranch: true,
sourceBranchRemoved: true,
- mergedBy: {},
- mergedAt: '',
- updatedAt: '',
+ mergedEvent: {
+ author: {},
+ updatedAt: 'mergedUpdatedAt',
+ formattedUpdatedAt: '',
+ },
+ updatedAt: 'mrUpdatedAt',
targetBranch,
};
@@ -170,5 +173,11 @@ describe('MRWidgetMerged', () => {
done();
});
});
+
+ it('should use mergedEvent updatedAt as tooltip title', () => {
+ expect(
+ el.querySelector('time').getAttribute('title'),
+ ).toBe('mergedUpdatedAt');
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index c607c9746a4..d7019ea408b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -11,6 +11,8 @@ const createComponent = (customConfig = {}) => {
isPipelineActive: false,
pipeline: null,
isPipelineFailed: false,
+ isPipelinePassing: false,
+ isMergeAllowed: true,
onlyAllowMergeIfPipelineSucceeds: false,
hasCI: false,
ciStatus: null,
@@ -68,6 +70,18 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('computed', () => {
+ describe('shouldShowMergeWhenPipelineSucceedsText', () => {
+ it('should return true with active pipeline', () => {
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy();
+ });
+
+ it('should return false with inactive pipeline', () => {
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy();
+ });
+ });
+
describe('commitMessageLinkTitle', () => {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
@@ -82,35 +96,84 @@ describe('MRWidgetReadyToMerge', () => {
});
});
+ describe('status', () => {
+ it('defaults to success', () => {
+ vm.mr.pipeline = true;
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns failed when MR has CI but also has an unknown status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.status).toEqual('failed');
+ });
+
+ it('returns default when MR has no pipeline', () => {
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns pending when pipeline is active', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.status).toEqual('pending');
+ });
+
+ it('returns failed when pipeline is failed', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineFailed = true;
+ expect(vm.status).toEqual('failed');
+ });
+ });
+
describe('mergeButtonClass', () => {
- const defaultClass = 'btn btn-small btn-success accept-merge-request';
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
- it('should return default class', () => {
+ it('defaults to success class', () => {
+ expect(vm.mergeButtonClass).toEqual(defaultClass);
+ });
+
+ it('returns success class for success status', () => {
vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
- it('should return failed class when MR has CI but also has an unknown status', () => {
+ it('returns info class for pending status', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.mergeButtonClass).toEqual(inActionClass);
+ });
+
+ it('returns failed class for failed status', () => {
vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
+ });
- it('should return default class when MR has no pipeline', () => {
- expect(vm.mergeButtonClass).toEqual(defaultClass);
+ describe('status icon', () => {
+ it('defaults to tick icon', () => {
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows tick for success status', () => {
+ vm.mr.pipeline = true;
+ expect(vm.iconClass).toEqual('success');
});
- it('should return in action class when pipeline is active', () => {
+ it('shows tick for pending status', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
- expect(vm.mergeButtonClass).toEqual(inActionClass);
+ expect(vm.iconClass).toEqual('success');
});
- it('should return failed class when pipeline is failed', () => {
- vm.mr.pipeline = {};
- vm.mr.isPipelineFailed = true;
- expect(vm.mergeButtonClass).toEqual(failedClass);
+ it('shows x for failed status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
+ });
+
+ it('shows x for merge not allowed', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
});
});
@@ -150,72 +213,54 @@ describe('MRWidgetReadyToMerge', () => {
describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => {
+ vm.mr.isMergeAllowed = true;
expect(vm.isMergeButtonDisabled).toBeFalsy();
});
it('should return true when there is no commit message', () => {
+ vm.mr.isMergeAllowed = true;
vm.commitMessage = '';
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
it('should return true if merge is not allowed', () => {
+ vm.mr.isMergeAllowed = false;
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelineFailed = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
- it('should return true when there vm instance is making request', () => {
+ it('should return true when the vm instance is making request', () => {
+ vm.mr.isMergeAllowed = true;
vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
});
-
- describe('Remove source branch checkbox', () => {
- describe('when user can merge but cannot delete branch', () => {
- it('isRemoveSourceBranchButtonDisabled should be true', () => {
- expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true);
- });
-
- it('should be disabled in the rendered output', () => {
- const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
- expect(checkboxElement.getAttribute('disabled')).toBe('disabled');
- });
- });
-
- describe('when user can merge and can delete branch', () => {
- beforeEach(() => {
- this.customVm = createComponent({
- mr: { canRemoveSourceBranch: true },
- });
- });
-
- it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
- });
-
- it('should be enabled in rendered output', () => {
- const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
- expect(checkboxElement.getAttribute('disabled')).toBeNull();
- });
- });
- });
});
describe('methods', () => {
- describe('isMergeAllowed', () => {
- it('should return false with initial data', () => {
- expect(vm.isMergeAllowed()).toBeTruthy();
+ describe('shouldShowMergeControls', () => {
+ it('should return false when an external pipeline is running and required to succeed', () => {
+ vm.mr.isMergeAllowed = false;
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeControls()).toBeFalsy();
});
- it('should return false when MR is set only merge when pipeline succeeds', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- expect(vm.isMergeAllowed()).toBeTruthy();
+ it('should return true when the build succeeded or build not required to succeed', () => {
+ vm.mr.isMergeAllowed = true;
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
});
- it('should return true true', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelineFailed = true;
- expect(vm.isMergeAllowed()).toBeFalsy();
+ it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
+ vm.mr.isMergeAllowed = false;
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
+ });
+
+ it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
+ vm.mr.isMergeAllowed = true;
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
});
});
@@ -419,4 +464,54 @@ describe('MRWidgetReadyToMerge', () => {
});
});
});
+
+ describe('Remove source branch checkbox', () => {
+ describe('when user can merge but cannot delete branch', () => {
+ it('isRemoveSourceBranchButtonDisabled should be true', () => {
+ expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true);
+ });
+
+ it('should be disabled in the rendered output', () => {
+ const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('when user can merge and can delete branch', () => {
+ beforeEach(() => {
+ this.customVm = createComponent({
+ mr: { canRemoveSourceBranch: true },
+ });
+ });
+
+ it('isRemoveSourceBranchButtonDisabled should be false', () => {
+ expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ });
+
+ it('should be enabled in rendered output', () => {
+ const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBeNull();
+ });
+ });
+ });
+
+ describe('Commit message area', () => {
+ it('when using merge commits, should show "Modify commit message" button', () => {
+ const customVm = createComponent({
+ mr: { ffOnlyEnabled: false },
+ });
+
+ expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull();
+ expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
+ });
+
+ it('when fast-forward merge is enabled, only show fast-forward message', () => {
+ const customVm = createComponent({
+ mr: { ffOnlyEnabled: true },
+ });
+
+ expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined();
+ expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 669ee248bf1..e4324e91502 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -59,23 +59,15 @@ describe('mrWidgetOptions', () => {
});
describe('shouldRenderPipelines', () => {
- it('should return true for the initial data', () => {
- expect(vm.shouldRenderPipelines).toBeTruthy();
- });
+ it('should return true when hasCI is true', () => {
+ vm.mr.hasCI = true;
- it('should return true when pipeline is empty but MR.hasCI is set to true', () => {
- vm.mr.pipeline = {};
expect(vm.shouldRenderPipelines).toBeTruthy();
});
- it('should return true when pipeline available', () => {
+ it('should return false when hasCI is false', () => {
vm.mr.hasCI = false;
- expect(vm.shouldRenderPipelines).toBeTruthy();
- });
- it('should return false when there is no pipeline', () => {
- vm.mr.pipeline = {};
- vm.mr.hasCI = false;
expect(vm.shouldRenderPipelines).toBeFalsy();
});
});
@@ -232,29 +224,41 @@ describe('mrWidgetOptions', () => {
describe('handleMounted', () => {
it('should call required methods to do the initial kick-off', () => {
spyOn(vm, 'initDeploymentsPolling');
- spyOn(vm, 'setFavicon');
+ spyOn(vm, 'setFaviconHelper');
vm.handleMounted();
- expect(vm.setFavicon).toHaveBeenCalled();
+ expect(vm.setFaviconHelper).toHaveBeenCalled();
expect(vm.initDeploymentsPolling).toHaveBeenCalled();
});
});
describe('setFavicon', () => {
+ let faviconElement;
+
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ document.body.appendChild(favicon);
+
+ faviconElement = document.getElementById('favicon');
+ });
+
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
+ });
+
it('should call setFavicon method', () => {
- spyOn(gl.utils, 'setFavicon');
- vm.setFavicon();
+ vm.setFaviconHelper();
- expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath);
+ expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath);
});
it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
- spyOn(gl.utils, 'setFavicon');
vm.mr.ciStatusFaviconPath = null;
- vm.setFavicon();
+ vm.setFaviconHelper();
- expect(gl.utils.setFavicon).not.toHaveBeenCalled();
+ expect(faviconElement.getAttribute('href')).toEqual(null);
});
});
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
index b63633c03b8..e667b4b3677 100644
--- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
+++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
@@ -31,6 +31,7 @@ describe('MRWidgetService', () => {
});
it('should have methods defined', () => {
+ window.history.pushState({}, null, '/');
const service = new MRWidgetService(mr);
expect(service.merge()).toBeDefined();
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index 56dd0198ae2..8e5614b20f0 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -18,5 +18,39 @@ describe('MergeRequestStore', () => {
store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
expect(store.hasSHAChanged).toBe(false);
});
+
+ describe('isPipelinePassing', () => {
+ it('is true when the CI status is `success`', () => {
+ store.setData({ ...mockData, ci_status: 'success' });
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is true when the CI status is `success_with_warnings`', () => {
+ store.setData({ ...mockData, ci_status: 'success_with_warnings' });
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is false when the CI status is `failed`', () => {
+ store.setData({ ...mockData, ci_status: 'failed' });
+ expect(store.isPipelinePassing).toBe(false);
+ });
+
+ it('is false when the CI status is anything except `success`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+ expect(store.isPipelinePassing).toBe(false);
+ });
+ });
+
+ describe('isPipelineSkipped', () => {
+ it('should set isPipelineSkipped=true when the CI status is `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'skipped' });
+ expect(store.isPipelineSkipped).toBe(true);
+ });
+
+ it('should set isPipelineSkipped=false when the CI status is anything except `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+ expect(store.isPipelineSkipped).toBe(false);
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
deleted file mode 100644
index 6df08f3ebe7..00000000000
--- a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue';
-
-describe('Confidential Issue Warning Component', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(confidentialIssue);
- vm = new Component().$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render confidential issue warning information', () => {
- expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash');
- expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
new file mode 100644
index 00000000000..2cf4d8e00ed
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+const IssueWarning = Vue.extend(issueWarning);
+
+function formatWarning(string) {
+ // Replace newlines with a space then replace multiple spaces with one space
+ return string.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' ');
+}
+
+describe('Issue Warning Component', () => {
+ describe('isLocked', () => {
+ it('should render locked issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
+ });
+ });
+
+ describe('isConfidential', () => {
+ it('should render confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
+ });
+ });
+
+ describe('isLocked and isConfidential', () => {
+ it('should render locked and confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i')).toBeFalsy();
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index ebd6c79077e..fe7a8c84c9e 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -296,7 +296,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
context 'project milestones' do
let(:milestone) { create(:milestone, project: project) }
- let(:reference) { milestone.to_reference }
+ let(:reference) { milestone.to_reference(format: :iid) }
include_examples 'reference parsing'
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 35a32a46eff..5f41e28fece 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -47,9 +47,11 @@ describe Banzai::Filter::SanitizationFilter do
describe 'custom whitelist' do
it 'customizes the whitelist only once' do
instance = described_class.new('Foo')
+ control_count = instance.whitelist[:transformers].size
+
3.times { instance.whitelist }
- expect(instance.whitelist[:transformers].size).to eq 4
+ expect(instance.whitelist[:transformers].size).to eq control_count
end
it 'sanitizes `class` attribute from all elements' do
@@ -63,8 +65,8 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq %q{<span>def</span>}
end
- it 'allows `style` attribute on table elements' do
- html = <<-HTML.strip_heredoc
+ it 'allows `text-align` property in `style` attribute on table elements' do
+ html = <<~HTML
<table>
<tr><th style="text-align: center">Head</th></tr>
<tr><td style="text-align: right">Body</th></tr>
@@ -77,6 +79,20 @@ describe Banzai::Filter::SanitizationFilter do
expect(doc.at_css('td')['style']).to eq 'text-align: right'
end
+ it 'disallows other properties in `style` attribute on table elements' do
+ html = <<~HTML
+ <table>
+ <tr><th style="text-align: foo">Head</th></tr>
+ <tr><td style="position: fixed; height: 50px; width: 50px; background: red; z-index: 999; font-size: 36px; text-align: center">Body</th></tr>
+ </table>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('th')['style']).to be_nil
+ expect(doc.at_css('td')['style']).to eq 'text-align: center'
+ end
+
it 'allows `span` elements' do
exp = act = %q{<span>Hello</span>}
expect(filter(act).to_html).to eq exp
@@ -87,6 +103,20 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq exp
end
+ it 'disallows the `name` attribute globally, allows on `a`' do
+ html = <<~HTML
+ <img name="getElementById" src="">
+ <span name="foo" class="bar">Hi</span>
+ <a name="foo" class="bar">Bye</a>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('img')).not_to have_attribute('name')
+ expect(doc.at_css('span')).not_to have_attribute('name')
+ expect(doc.at_css('a')).to have_attribute('name')
+ end
+
it 'allows `summary` elements' do
exp = act = '<summary>summary line</summary>'
expect(filter(act).to_html).to eq exp
diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
new file mode 100644
index 00000000000..6a11ca2f9d5
--- /dev/null
+++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::EmailPipeline do
+ describe '.filters' do
+ it 'returns the expected type' do
+ expect(described_class.filters).to be_kind_of(Banzai::FilterArray)
+ end
+
+ it 'excludes ImageLazyLoadFilter' do
+ expect(described_class.filters).not_to be_empty
+ expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter)
+ end
+ end
+end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index da42272bbef..81a04a2d46d 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -31,7 +31,14 @@ describe Banzai::Renderer do
let(:object) { fake_object(fresh: false) }
it 'caches and returns the result' do
- expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
+ expect(object).to receive(:refresh_markdown_cache!)
+
+ is_expected.to eq('field_html')
+ end
+
+ it "skips database caching on a GitLab read-only instance" do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
deleted file mode 100644
index e49ecadde20..00000000000
--- a/spec/lib/ci/ansi2html_spec.rb
+++ /dev/null
@@ -1,220 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Ansi2html do
- subject { described_class }
-
- it "prints non-ansi as-is" do
- expect(convert_html("Hello")).to eq('Hello')
- end
-
- it "strips non-color-changing controll sequences" do
- expect(convert_html("Hello \e[2Kworld")).to eq('Hello world')
- end
-
- it "prints simply red" do
- expect(convert_html("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>')
- end
-
- it "prints simply red without trailing reset" do
- expect(convert_html("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
- end
-
- it "prints simply yellow" do
- expect(convert_html("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
- end
-
- it "prints default on blue" do
- expect(convert_html("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
- end
-
- it "prints red on blue" do
- expect(convert_html("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
- end
-
- it "resets colors after red on blue" do
- expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
- end
-
- it "performs color change from red/blue to yellow/blue" do
- expect(convert_html("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
- end
-
- it "performs color change from red/blue to yellow/green" do
- expect(convert_html("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
- end
-
- it "performs color change from red/blue to reset to yellow/green" do
- expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
- end
-
- it "ignores unsupported codes" do
- expect(convert_html("\e[51mHello\e[0m")).to eq('Hello')
- end
-
- it "prints light red" do
- expect(convert_html("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>')
- end
-
- it "prints default on light red" do
- expect(convert_html("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
- end
-
- it "performs color change from red/blue to default/blue" do
- expect(convert_html("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
- end
-
- it "performs color change from light red/blue to default/blue" do
- expect(convert_html("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
- end
-
- it "prints bold text" do
- expect(convert_html("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
- end
-
- it "resets bold text" do
- expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
- expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
- end
-
- it "prints italic text" do
- expect(convert_html("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
- end
-
- it "resets italic text" do
- expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
- end
-
- it "prints underlined text" do
- expect(convert_html("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
- end
-
- it "resets underlined text" do
- expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
- end
-
- it "prints concealed text" do
- expect(convert_html("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
- end
-
- it "resets concealed text" do
- expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
- end
-
- it "prints crossed-out text" do
- expect(convert_html("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
- end
-
- it "resets crossed-out text" do
- expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
- end
-
- it "can print 256 xterm fg colors" do
- expect(convert_html("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
- end
-
- it "can print 256 xterm fg colors on normal magenta background" do
- expect(convert_html("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
- end
-
- it "can print 256 xterm bg colors" do
- expect(convert_html("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
- end
-
- it "can print 256 xterm bg colors on normal magenta foreground" do
- expect(convert_html("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
- end
-
- it "prints bold colored text vividly" do
- expect(convert_html("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
- end
-
- it "prints bold light colored text correctly" do
- expect(convert_html("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
- end
-
- it "prints &lt;" do
- expect(convert_html("<")).to eq('&lt;')
- end
-
- it "replaces newlines with line break tags" do
- expect(convert_html("\n")).to eq('<br>')
- end
-
- it "groups carriage returns with newlines" do
- expect(convert_html("\r\n")).to eq('<br>')
- end
-
- describe "incremental update" do
- shared_examples 'stateable converter' do
- let(:pass1_stream) { StringIO.new(pre_text) }
- let(:pass2_stream) { StringIO.new(pre_text + text) }
- let(:pass1) { subject.convert(pass1_stream) }
- let(:pass2) { subject.convert(pass2_stream, pass1.state) }
-
- it "to returns html to append" do
- expect(pass2.append).to be_truthy
- expect(pass2.html).to eq(html)
- expect(pass1.html + pass2.html).to eq(pre_html + html)
- end
- end
-
- context "with split word" do
- let(:pre_text) { "\e[1mHello" }
- let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
- let(:text) { "\e[1mWorld" }
- let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" }
-
- it_behaves_like 'stateable converter'
- end
-
- context "with split sequence" do
- let(:pre_text) { "\e[1m" }
- let(:pre_html) { "<span class=\"term-bold\"></span>" }
- let(:text) { "Hello" }
- let(:html) { "<span class=\"term-bold\">Hello</span>" }
-
- it_behaves_like 'stateable converter'
- end
-
- context "with partial sequence" do
- let(:pre_text) { "Hello\e" }
- let(:pre_html) { "Hello" }
- let(:text) { "[1m World" }
- let(:html) { "<span class=\"term-bold\"> World</span>" }
-
- it_behaves_like 'stateable converter'
- end
-
- context 'with new line' do
- let(:pre_text) { "Hello\r" }
- let(:pre_html) { "Hello\r" }
- let(:text) { "\nWorld" }
- let(:html) { "<br>World" }
-
- it_behaves_like 'stateable converter'
- end
- end
-
- describe "truncates" do
- let(:text) { "Hello World" }
- let(:stream) { StringIO.new(text) }
- let(:subject) { described_class.convert(stream) }
-
- before do
- stream.seek(3, IO::SEEK_SET)
- end
-
- it "returns truncated output" do
- expect(subject.truncated).to be_truthy
- end
-
- it "does not append output" do
- expect(subject.append).to be_falsey
- end
- end
-
- def convert_html(data)
- stream = StringIO.new(data)
- subject.convert(stream).html
- end
-end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
deleted file mode 100644
index f0769deef21..00000000000
--- a/spec/lib/ci/charts_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Charts do
- context "pipeline_times" do
- let(:project) { create(:project) }
- let(:chart) { Ci::Charts::PipelineTime.new(project) }
-
- subject { chart.pipeline_times }
-
- before do
- create(:ci_empty_pipeline, project: project, duration: 120)
- end
-
- it 'returns pipeline times in minutes' do
- is_expected.to contain_exactly(2)
- end
-
- it 'handles nil pipeline times' do
- create(:ci_empty_pipeline, project: project, duration: nil)
-
- is_expected.to contain_exactly(2, 0)
- end
- end
-end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
deleted file mode 100644
index 1efd3113a43..00000000000
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ /dev/null
@@ -1,1697 +0,0 @@
-require 'spec_helper'
-
-module Ci
- describe GitlabCiYamlProcessor, :lib do
- subject { described_class.new(config, path) }
- let(:path) { 'path' }
-
- describe 'our current .gitlab-ci.yml' do
- let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
-
- it 'is valid' do
- error_message = described_class.validation_message(config)
-
- expect(error_message).to be_nil
- end
- end
-
- describe '#build_attributes' do
- subject { described_class.new(config, path).build_attributes(:rspec) }
-
- describe 'coverage entry' do
- describe 'code coverage regexp' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- coverage: '/Code coverage: \d+\.\d+/' })
- end
-
- it 'includes coverage regexp in build attributes' do
- expect(subject)
- .to include(coverage_regex: 'Code coverage: \d+\.\d+')
- end
- end
- end
-
- describe 'retry entry' do
- context 'when retry count is specified' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec', retry: 1 })
- end
-
- it 'includes retry count in build options attribute' do
- expect(subject[:options]).to include(retry: 1)
- end
- end
-
- context 'when retry count is not specified' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec' })
- end
-
- it 'does not persist retry count in the database' do
- expect(subject[:options]).not_to have_key(:retry)
- end
- end
- end
-
- describe 'allow failure entry' do
- context 'when job is a manual action' do
- context 'when allow_failure is defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- when: 'manual',
- allow_failure: false })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
-
- context 'when allow_failure is not defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- when: 'manual' })
- end
-
- it 'is allowed to fail' do
- expect(subject[:allow_failure]).to be true
- end
- end
- end
-
- context 'when job is not a manual action' do
- context 'when allow_failure is defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- allow_failure: false })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
-
- context 'when allow_failure is not defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec' })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
- end
- end
- end
-
- describe '#stage_seeds' do
- context 'when no refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
- rspec: { stage: 'test', script: 'rspec' },
- spinach: { stage: 'test', script: 'spinach' })
- end
-
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- it 'correctly fabricates a stage seeds object' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.second.stage[:name]).to eq 'deploy'
- expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
- expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, ref: 'feature', tag: true)
- end
-
- it 'returns stage seeds only assigned to master to master' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when source policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
- spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, source: :schedule)
- end
-
- it 'returns stage seeds only assigned to schedules' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when kubernetes policy is specified' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- let(:config) do
- YAML.dump(
- spinach: { stage: 'test', script: 'spinach' },
- production: {
- stage: 'deploy',
- script: 'cap',
- only: { kubernetes: 'active' }
- }
- )
- end
-
- context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when kubernetes is not active' do
- it 'does not return seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
- end
- end
-
- describe "#builds_for_stage_and_ref" do
- let(:type) { 'test' }
-
- it "returns builds if no branch specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec" }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- describe 'only' do
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: %w(master deploy) }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "returns builds if only has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if only has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["tags"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if only has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
- end
- end
-
- it "does not return builds if only has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
- end
- end
-
- it "returns builds if only has current repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@path"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if only has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns build only for specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: %w(master deploy) },
- staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
- production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, 'fork')
-
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:only) { 1 }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:only) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:only) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
- end
- end
-
- describe 'except' do
- it "returns builds if except has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "returns builds if except has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["/^deploy$/"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "does not return builds if except has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["master"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "does not return builds if except has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: %w(master deploy) }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "does not return builds if except has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if except has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["tags"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if except has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
- end
- end
-
- it "returns builds if except has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
- end
- end
-
- it "does not return builds if except has current repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@path"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if except has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "returns build except specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
- staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, 'fork')
-
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", except: except } } }
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:except) { 1 }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:except) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:except) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
- end
- end
- end
-
- describe "Scripts handling" do
- let(:config_data) { YAML.dump(config) }
- let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
-
- subject { config_processor.builds_for_stage_and_ref("test", "master").first }
-
- describe "before_script" do
- context "in global context" do
- let(:config) do
- {
- before_script: ["global script"],
- test: { script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("global script\nscript")
- end
- end
-
- context "overwritten in local context" do
- let(:config) do
- {
- before_script: ["global script"],
- test: { before_script: ["local script"], script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("local script\nscript")
- end
- end
- end
-
- describe "script" do
- let(:config) do
- {
- test: { script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("script")
- end
- end
-
- describe "after_script" do
- context "in global context" do
- let(:config) do
- {
- after_script: ["after_script"],
- test: { script: ["script"] }
- }
- end
-
- it "return after_script in options" do
- expect(subject[:options][:after_script]).to eq(["after_script"])
- end
- end
-
- context "overwritten in local context" do
- let(:config) do
- {
- after_script: ["local after_script"],
- test: { after_script: ["local after_script"], script: ["script"] }
- }
- end
-
- it "return after_script in options" do
- expect(subject[:options][:after_script]).to eq(["local after_script"])
- end
- end
- end
- end
-
- describe "Image and service handling" do
- context "when extended docker configuration is used" do
- it "returns image and service when defined" do
- config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
- services: ["mysql", { name: "docker:dind", alias: "docker",
- entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }],
- before_script: ["pwd"],
- rspec: { script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "mysql" },
- { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- it "returns image and service when overridden for job" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "postgresql", alias: "db-pg",
- entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
- script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] },
- { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context "when etended docker configuration is not used" do
- it "returns image and service when defined" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql", "docker:dind"],
- before_script: ["pwd"],
- rspec: { script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1" },
- services: [{ name: "mysql" }, { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- it "returns image and service when overridden for job" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.5" },
- services: [{ name: "postgresql" }, { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
- end
- end
-
- describe 'Variables' do
- let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }
-
- subject { config_processor.builds.first[:yaml_variables] }
-
- context 'when global variables are defined' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
- let(:config) do
- {
- variables: variables,
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
- end
-
- it 'returns global variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when job and global variables are defined' do
- let(:global_variables) do
- { 'VAR1' => 'global1', 'VAR3' => 'global3' }
- end
- let(:job_variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
- let(:config) do
- {
- before_script: ['pwd'],
- variables: global_variables,
- rspec: { script: 'rspec', variables: job_variables }
- }
- end
-
- it 'returns all unique variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR3', value: 'global3', public: true },
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when job variables are defined' do
- let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec', variables: variables }
- }
- end
-
- context 'when syntax is correct' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
-
- it 'returns job variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when syntax is incorrect' do
- context 'when variables defined but invalid' do
- let(:variables) do
- %w(VAR1 value1 VAR2 value2)
- end
-
- it 'raises error' do
- expect { subject }
- .to raise_error(GitlabCiYamlProcessor::ValidationError,
- /jobs:rspec:variables config should be a hash of key value pairs/)
- end
- end
-
- context 'when variables key defined but value not specified' do
- let(:variables) do
- nil
- end
-
- it 'returns empty array' do
- ##
- # When variables config is empty, we assume this is a valid
- # configuration, see issue #18775
- #
- expect(subject).to be_an_instance_of(Array)
- expect(subject).to be_empty
- end
- end
- end
- end
-
- context 'when job variables are not defined' do
- let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
- end
-
- it 'returns empty array' do
- expect(subject).to be_an_instance_of(Array)
- expect(subject).to be_empty
- end
- end
- end
-
- describe "When" do
- %w(on_success on_failure always).each do |when_state|
- it "returns #{when_state} when defined" do
- config = YAML.dump({
- rspec: { script: "rspec", when: when_state }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- builds = config_processor.builds_for_stage_and_ref("test", "master")
- expect(builds.size).to eq(1)
- expect(builds.first[:when]).to eq(when_state)
- end
- end
- end
-
- describe 'cache' do
- context 'when cache definition has unknown keys' do
- it 'raises relevant validation error' do
- config = YAML.dump(
- { cache: { untracked: true, invalid: 'key' },
- rspec: { script: 'rspec' } })
-
- expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
- GitlabCiYamlProcessor::ValidationError,
- 'cache config contains unknown keys: invalid'
- )
- end
- end
-
- it "returns cache when defined globally" do
- config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- rspec: {
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
- untracked: true,
- key: 'key',
- policy: 'pull-push'
- )
- end
-
- it "returns cache when defined in a job" do
- config = YAML.dump({
- rspec: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
- untracked: true,
- key: 'key',
- policy: 'pull-push'
- )
- end
-
- it "overwrite cache when defined for a job and globally" do
- config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
- rspec: {
- script: "rspec",
- cache: { paths: ["test/"], untracked: false, key: 'local' }
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["test/"],
- untracked: false,
- key: 'local',
- policy: 'pull-push'
- )
- end
- end
-
- describe "Artifacts" do
- it "returns artifacts when defined" do
- config = YAML.dump({
- image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: {
- artifacts: {
- paths: ["logs/", "binaries/"],
- untracked: true,
- name: "custom_name",
- expire_in: "7d"
- },
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1" },
- services: [{ name: "mysql" }],
- artifacts: {
- name: "custom_name",
- paths: ["logs/", "binaries/"],
- untracked: true,
- expire_in: "7d"
- }
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
-
- %w[on_success on_failure always].each do |when_state|
- it "returns artifacts for when #{when_state} defined" do
- config = YAML.dump({
- rspec: {
- script: "rspec",
- artifacts: { paths: ["logs/", "binaries/"], when: when_state }
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- builds = config_processor.builds_for_stage_and_ref("test", "master")
- expect(builds.size).to eq(1)
- expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
- end
- end
- end
-
- describe '#environment' do
- let(:config) do
- {
- deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
- }
- end
-
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
-
- context 'when a production environment is specified' do
- let(:environment) { 'production' }
-
- it 'does return production' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment)
- expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
- end
- end
-
- context 'when hash is specified' do
- let(:environment) do
- { name: 'production',
- url: 'http://production.gitlab.com' }
- end
-
- it 'does return production and URL' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment[:name])
- expect(builds.first[:options]).to include(environment: environment)
- end
-
- context 'the url has a port as variable' do
- let(:environment) do
- { name: 'production',
- url: 'http://production.gitlab.com:$PORT' }
- end
-
- it 'allows a variable for the port' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment[:name])
- expect(builds.first[:options]).to include(environment: environment)
- end
- end
- end
-
- context 'when no environment is specified' do
- let(:environment) { nil }
-
- it 'does return nil environment' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to be_nil
- end
- end
-
- context 'is not a string' do
- let(:environment) { 1 }
-
- it 'raises error' do
- expect { builds }.to raise_error(
- 'jobs:deploy_to_production:environment config should be a hash or a string')
- end
- end
-
- context 'is not a valid string' do
- let(:environment) { 'production:staging' }
-
- it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
- end
- end
-
- context 'when on_stop is specified' do
- let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
- let(:config) { { review: review, close_review: close_review }.compact }
-
- context 'with matching job' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
-
- it 'does return a list of builds' do
- expect(builds.size).to eq(2)
- expect(builds.first[:environment]).to eq('review')
- end
- end
-
- context 'without matching job' do
- let(:close_review) { nil }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
- end
- end
-
- context 'with close job without environment' do
- let(:close_review) { { stage: 'deploy', script: 'test' } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
- end
- end
-
- context 'with close job for different environment' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
- end
- end
-
- context 'with close job without stop action' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
- end
- end
- end
- end
-
- describe "Dependencies" do
- let(:config) do
- {
- build1: { stage: 'build', script: 'test' },
- build2: { stage: 'build', script: 'test' },
- test1: { stage: 'test', script: 'test', dependencies: dependencies },
- test2: { stage: 'test', script: 'test' },
- deploy: { stage: 'test', script: 'test' }
- }
- end
-
- subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'no dependencies' do
- let(:dependencies) { }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'dependencies to builds' do
- let(:dependencies) { %w(build1 build2) }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'dependencies to builds defined as symbols' do
- let(:dependencies) { [:build1, :build2] }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'undefined dependency' do
- let(:dependencies) { ['undefined'] }
-
- it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
- end
-
- context 'dependencies to deploy' do
- let(:dependencies) { ['deploy'] }
-
- it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
- end
- end
-
- describe "Hidden jobs" do
- let(:config_processor) { GitlabCiYamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("test", "master") }
-
- shared_examples 'hidden_job_handling' do
- it "doesn't create jobs that start with dot" do
- expect(subject.size).to eq(1)
- expect(subject.first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "normal_job",
- commands: "test",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["test"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context 'when hidden job have a script definition' do
- let(:config) do
- YAML.dump({
- '.hidden_job' => { image: 'ruby:2.1', script: 'test' },
- 'normal_job' => { script: 'test' }
- })
- end
-
- it_behaves_like 'hidden_job_handling'
- end
-
- context "when hidden job doesn't have a script definition" do
- let(:config) do
- YAML.dump({
- '.hidden_job' => { image: 'ruby:2.1' },
- 'normal_job' => { script: 'test' }
- })
- end
-
- it_behaves_like 'hidden_job_handling'
- end
- end
-
- describe "YAML Alias/Anchor" do
- let(:config_processor) { GitlabCiYamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("build", "master") }
-
- shared_examples 'job_templates_handling' do
- it "is correctly supported for jobs" do
- expect(subject.size).to eq(2)
- expect(subject.first).to eq({
- stage: "build",
- stage_idx: 0,
- name: "job1",
- commands: "execute-script-for-job",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["execute-script-for-job"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- expect(subject.second).to eq({
- stage: "build",
- stage_idx: 0,
- name: "job2",
- commands: "execute-script-for-job",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["execute-script-for-job"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context 'when template is a job' do
- let(:config) do
- <<EOT
-job1: &JOBTMPL
- stage: build
- script: execute-script-for-job
-
-job2: *JOBTMPL
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
-
- context 'when template is a hidden job' do
- let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
- script: execute-script-for-job
-
-job1: *JOBTMPL
-
-job2: *JOBTMPL
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
-
- context 'when job adds its own keys to a template definition' do
- let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
-
-job1:
- <<: *JOBTMPL
- script: execute-script-for-job
-
-job2:
- <<: *JOBTMPL
- script: execute-script-for-job
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
- end
-
- describe "Error handling" do
- it "fails to parse YAML" do
- expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
- end
-
- it "indicates that object is invalid" do
- expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
- end
-
- it "returns errors if tags parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
- end
-
- it "returns errors if before_script parameter is invalid" do
- config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
- end
-
- it "returns errors if job before_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
- end
-
- it "returns errors if after_script parameter is invalid" do
- config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
- end
-
- it "returns errors if job after_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
- end
-
- it "returns errors if image parameter is invalid" do
- config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string")
- end
-
- it "returns errors if job name is blank" do
- config = YAML.dump({ '' => { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
- end
-
- it "returns errors if job name is non-string" do
- config = YAML.dump({ 10 => { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
- end
-
- it "returns errors if job image parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
- end
-
- it "returns errors if services parameter is not an array" do
- config = YAML.dump({ services: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array")
- end
-
- it "returns errors if services parameter is not an array of strings" do
- config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
- end
-
- it "returns errors if job services parameter is not an array" do
- config = YAML.dump({ rspec: { script: "test", services: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
- end
-
- it "returns errors if job services parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
- end
-
- it "returns error if job configuration is invalid" do
- config = YAML.dump({ extra: "bundle update" })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
- end
-
- it "returns errors if services configuration is not correct" do
- config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array")
- end
-
- it "returns errors if there are no jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"] })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
- end
-
- it "returns errors if there are no visible jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
- end
-
- it "returns errors if job allow_failure parameter is not an boolean" do
- config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
- end
-
- it "returns errors if job stage is not a string" do
- config = YAML.dump({ rspec: { script: "test", type: 1 } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
- end
-
- it "returns errors if job stage is not a pre-defined stage" do
- config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
- end
-
- it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
- end
-
- it "returns errors if stages is not an array" do
- config = YAML.dump({ stages: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
- end
-
- it "returns errors if stages is not an array of strings" do
- config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
- end
-
- it "returns errors if variables is not a map" do
- config = YAML.dump({ variables: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
- end
-
- it "returns errors if variables is not a map of key-value strings" do
- config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
- end
-
- it "returns errors if job when is not on_success, on_failure or always" do
- config = YAML.dump({ rspec: { script: "test", when: 1 } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
- end
-
- it "returns errors if job artifacts:name is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
- end
-
- it "returns errors if job artifacts:when is not an a predefined value" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
- end
-
- it "returns errors if job artifacts:expire_in is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
- end
-
- it "returns errors if job artifacts:expire_in is not an a valid duration" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
- end
-
- it "returns errors if job artifacts:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
- end
-
- it "returns errors if job artifacts:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
- end
-
- it "returns errors if cache:untracked is not an array of strings" do
- config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
- end
-
- it "returns errors if cache:paths is not an array of strings" do
- config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings")
- end
-
- it "returns errors if cache:key is not a string" do
- config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
- end
-
- it "returns errors if job cache:key is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
- end
-
- it "returns errors if job cache:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
- end
-
- it "returns errors if job cache:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
- end
-
- it "returns errors if job dependencies is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
- end
- end
-
- describe "Validate configuration templates" do
- templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
-
- templates.each do |file|
- it "does not return errors for #{file}" do
- file = File.read(file)
-
- expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error
- end
- end
- end
-
- describe "#validation_message" do
- context "when the YAML could not be parsed" do
- it "returns an error about invalid configutaion" do
- content = YAML.dump("invalid: yaml: test")
-
- expect(GitlabCiYamlProcessor.validation_message(content))
- .to eq "Invalid configuration format"
- end
- end
-
- context "when the tags parameter is invalid" do
- it "returns an error about invalid tags" do
- content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
-
- expect(GitlabCiYamlProcessor.validation_message(content))
- .to eq "jobs:rspec tags should be an array of strings"
- end
- end
-
- context "when YAML content is empty" do
- it "returns an error about missing content" do
- expect(GitlabCiYamlProcessor.validation_message(''))
- .to eq "Please provide content of .gitlab-ci.yml"
- end
- end
-
- context "when the YAML is valid" do
- it "does not return any errors" do
- content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
-
- expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb
deleted file mode 100644
index f7b753b022b..00000000000
--- a/spec/lib/ci/mask_secret_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'spec_helper'
-
-describe Ci::MaskSecret do
- subject { described_class }
-
- describe '#mask' do
- it 'masks exact number of characters' do
- expect(mask('token', 'oke')).to eq('txxxn')
- end
-
- it 'masks multiple occurrences' do
- expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
- end
-
- it 'does not mask if not found' do
- expect(mask('token', 'not')).to eq('token')
- end
-
- it 'does support null token' do
- expect(mask('token', nil)).to eq('token')
- end
-
- def mask(value, token)
- subject.mask!(value.dup, token)
- end
- end
-end
diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb
new file mode 100644
index 00000000000..b846096fe25
--- /dev/null
+++ b/spec/lib/github/client_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Github::Client do
+ let(:connection) { spy }
+ let(:rate_limit) { double(get: [false, 1]) }
+ let(:client) { described_class.new({}) }
+ let(:results) { double }
+ let(:response) { double }
+
+ before do
+ allow(Faraday).to receive(:new).and_return(connection)
+ allow(Github::RateLimit).to receive(:new).with(connection).and_return(rate_limit)
+ end
+
+ describe '#get' do
+ before do
+ allow(Github::Response).to receive(:new).with(results).and_return(response)
+ end
+
+ it 'uses a default per_page param' do
+ expect(connection).to receive(:get).with('/foo', per_page: 100).and_return(results)
+
+ expect(client.get('/foo')).to eq(response)
+ end
+
+ context 'with per_page given' do
+ it 'overwrites the default per_page' do
+ expect(connection).to receive(:get).with('/foo', per_page: 30).and_return(results)
+
+ expect(client.get('/foo', per_page: 30)).to eq(response)
+ end
+ end
+ end
+end
diff --git a/spec/lib/github/import/legacy_diff_note_spec.rb b/spec/lib/github/import/legacy_diff_note_spec.rb
new file mode 100644
index 00000000000..8c50b46cacb
--- /dev/null
+++ b/spec/lib/github/import/legacy_diff_note_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Github::Import::LegacyDiffNote do
+ describe '#type' do
+ it 'returns the original note type' do
+ expect(described_class.new.type).to eq('LegacyDiffNote')
+ end
+ end
+end
diff --git a/spec/lib/github/import/note_spec.rb b/spec/lib/github/import/note_spec.rb
new file mode 100644
index 00000000000..fcdccd9e097
--- /dev/null
+++ b/spec/lib/github/import/note_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Github::Import::Note do
+ describe '#type' do
+ it 'returns the original note type' do
+ expect(described_class.new.type).to eq('Note')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f685bb83d0d..af1db2c3455 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -16,12 +16,32 @@ describe Gitlab::Auth do
expect(subject::DEFAULT_SCOPES).to eq [:api]
end
- it 'OPTIONAL_SCOPES contains all non-default scopes' do
- expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid]
+ it 'optional_scopes contains all non-default scopes' do
+ stub_container_registry_config(enabled: true)
+
+ expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
end
- it 'REGISTRY_SCOPES contains all registry related scopes' do
- expect(subject::REGISTRY_SCOPES).to eq %i[read_registry]
+ context 'registry_scopes' do
+ context 'when registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it 'is empty' do
+ expect(subject.registry_scopes).to eq []
+ end
+ end
+
+ context 'when registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ it 'contains all registry related scopes' do
+ expect(subject.registry_scopes).to eq %i[read_registry]
+ end
+ end
end
end
@@ -147,11 +167,17 @@ describe Gitlab::Auth do
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
end
- it 'succeeds for personal access tokens with the `read_registry` scope' do
- personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
+ context 'when registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ it 'succeeds for personal access tokens with the `read_registry` scope' do
+ personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
+ end
end
it 'succeeds if it is an impersonation token' do
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
new file mode 100644
index 00000000000..1a4ea2bac48
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do
+ let(:migration) { described_class.new }
+
+ let(:base1) { create(:project) }
+ let(:base1_fork1) { create(:project) }
+ let(:base1_fork2) { create(:project) }
+
+ let(:base2) { create(:project) }
+ let(:base2_fork1) { create(:project) }
+ let(:base2_fork2) { create(:project) }
+
+ let(:fork_of_fork) { create(:project) }
+ let(:fork_of_fork2) { create(:project) }
+ let(:second_level_fork) { create(:project) }
+ let(:third_level_fork) { create(:project) }
+
+ let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
+ let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
+
+ let!(:forked_project_links) { table(:forked_project_links) }
+ let!(:fork_networks) { table(:fork_networks) }
+ let!(:fork_network_members) { table(:fork_network_members) }
+
+ before do
+ # The fork-network relation created for the forked project
+ fork_networks.create(id: 1, root_project_id: base1.id)
+ fork_network_members.create(project_id: base1.id, fork_network_id: 1)
+ fork_networks.create(id: 2, root_project_id: base2.id)
+ fork_network_members.create(project_id: base2.id, fork_network_id: 2)
+
+ # Normal fork links
+ forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id)
+ forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id)
+ forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id)
+ forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id)
+
+ # Fork links
+ forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id)
+ forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id)
+
+ # Forks 3 levels down
+ forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id)
+ forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id)
+
+ migration.perform(1, 8)
+ end
+
+ it 'creates a memberships for the direct forks' do
+ base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1_fork1.id)
+ base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1_fork2.id)
+ base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2_fork1.id)
+ base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2_fork2.id)
+
+ expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id)
+ expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id)
+ expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id)
+ expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id)
+ end
+
+ it 'adds the fork network members for forks of forks' do
+ fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id,
+ fork_network_id: fork_network1.id)
+ fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id,
+ fork_network_id: fork_network1.id)
+ second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id,
+ fork_network_id: fork_network1.id)
+ third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id,
+ fork_network_id: fork_network1.id)
+
+ expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id)
+ expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id)
+ expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id)
+ expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id)
+ end
+
+ it 'reschedules itself when there are missing members' do
+ allow(migration).to receive(:missing_members?).and_return(true)
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3])
+
+ migration.perform(1, 3)
+ end
+
+ it 'can be repeated without effect' do
+ expect { fork_network_members.count }.not_to change { migration.perform(1, 7) }
+ end
+
+ it 'knows it is finished for this range' do
+ expect(migration.missing_members?(1, 7)).to be_falsy
+ end
+
+ context 'with more forks' do
+ before do
+ forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ end
+
+ it 'only processes a single batch of links at a time' do
+ expect(fork_network_members.count).to eq(10)
+
+ migration.perform(8, 10)
+
+ expect(fork_network_members.count).to eq(12)
+ end
+
+ it 'knows when not all memberships withing a batch have been created' do
+ expect(migration.missing_members?(8, 10)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
new file mode 100644
index 00000000000..26d48cc8201
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do
+ context 'when GpgKey exists' do
+ let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) }
+
+ before do
+ GpgKeySubkey.destroy_all
+ end
+
+ it 'generate the subkeys' do
+ expect do
+ described_class.new.perform(gpg_key.id)
+ end.to change { gpg_key.subkeys.count }.from(0).to(2)
+ end
+
+ it 'schedules the signature update worker' do
+ expect(InvalidGpgSignatureUpdateWorker).to receive(:perform_async).with(gpg_key.id)
+
+ described_class.new.perform(gpg_key.id)
+ end
+ end
+
+ context 'when GpgKey does not exist' do
+ it 'does not do anything' do
+ expect(Gitlab::Gpg).not_to receive(:subkeys_from_key)
+ expect(InvalidGpgSignatureUpdateWorker).not_to receive(:perform_async)
+
+ described_class.new.perform(123)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
new file mode 100644
index 00000000000..5c471cbdeda
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ before do
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'deletes the conflicting redirect_routes in the range' do
+ expect(redirect_routes.count).to eq(8)
+
+ expect do
+ described_class.new.perform(1, 3)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2)
+
+ expect do
+ described_class.new.perform(4, 5)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0)
+
+ expect(redirect_routes.count).to eq(3)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index c0427639746..d2e7243ee05 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
+describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate do
describe '#perform' do
- set(:merge_request) { create(:merge_request) }
- set(:merge_request_diff) { merge_request.merge_request_diff }
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_diff) { merge_request.merge_request_diff }
let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) }
def diffs_to_hashes(diffs)
@@ -70,8 +70,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
before do
merge_request.reload_diff(true)
- convert_to_yaml(start_id, merge_request_diff.commits, merge_request_diff.diffs)
- convert_to_yaml(stop_id, updated_merge_request_diff.commits, updated_merge_request_diff.diffs)
+ convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files))
+ convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files))
MergeRequestDiffCommit.delete_all
MergeRequestDiffFile.delete_all
@@ -80,10 +80,32 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when BUFFER_ROWS is exceeded' do
before do
stub_const("#{described_class}::BUFFER_ROWS", 1)
+
+ allow(Gitlab::Database).to receive(:bulk_insert).and_call_original
+ end
+
+ it 'inserts commit rows in chunks of BUFFER_ROWS' do
+ # There are 29 commits in each diff, so we should have slices of 20 + 9 + 20 + 9.
+ stub_const("#{described_class}::BUFFER_ROWS", 20)
+
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with('merge_request_diff_commits', anything)
+ .exactly(4)
+ .times
+ .and_call_original
+
+ subject.perform(start_id, stop_id)
end
- it 'updates and continues' do
- expect(described_class::MergeRequestDiff).to receive(:transaction).twice
+ it 'inserts diff rows in chunks of DIFF_FILE_BUFFER_ROWS' do
+ # There are 20 files in each diff, so we should have slices of 20 + 20.
+ stub_const("#{described_class}::DIFF_FILE_BUFFER_ROWS", 20)
+
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with('merge_request_diff_files', anything)
+ .exactly(2)
+ .times
+ .and_call_original
subject.perform(start_id, stop_id)
end
@@ -91,27 +113,87 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when BUFFER_ROWS is not exceeded' do
it 'only updates once' do
- expect(described_class::MergeRequestDiff).to receive(:transaction).once
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with('merge_request_diff_commits', anything)
+ .once
+ .and_call_original
+
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with('merge_request_diff_files', anything)
+ .once
+ .and_call_original
subject.perform(start_id, stop_id)
end
end
- end
- context 'when the merge request diff update fails' do
- before do
- allow(described_class::MergeRequestDiff)
- .to receive(:update_all).and_raise(ActiveRecord::Rollback)
- end
+ context 'when some rows were already inserted due to a previous failure' do
+ before do
+ subject.perform(start_id, stop_id)
- it 'does not add any diff commits' do
- expect { subject.perform(merge_request_diff.id, merge_request_diff.id) }
- .not_to change { MergeRequestDiffCommit.count }
+ convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files))
+ convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files))
+ end
+
+ it 'does not raise' do
+ expect { subject.perform(start_id, stop_id) }.not_to raise_exception
+ end
+
+ it 'logs a message' do
+ expect(Rails.logger).to receive(:info)
+ .with(
+ a_string_matching(described_class.name).and(matching([start_id, stop_id].inspect))
+ )
+ .twice
+
+ subject.perform(start_id, stop_id)
+ end
+
+ it 'ends up with the correct rows' do
+ expect(updated_merge_request_diff.commits.count).to eq(29)
+ expect(updated_merge_request_diff.raw_diffs.count).to eq(20)
+ end
end
- it 'does not add any diff files' do
- expect { subject.perform(merge_request_diff.id, merge_request_diff.id) }
- .not_to change { MergeRequestDiffFile.count }
+ context 'when the merge request diff update fails' do
+ let(:exception) { ActiveRecord::RecordNotFound }
+
+ let(:perform_ignoring_exceptions) do
+ begin
+ subject.perform(start_id, stop_id)
+ rescue described_class::Error
+ end
+ end
+
+ before do
+ allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation)
+ .to receive(:update_all).and_raise(exception)
+ end
+
+ it 'raises an error' do
+ expect { subject.perform(start_id, stop_id) }
+ .to raise_exception(described_class::Error)
+ end
+
+ it 'logs the error' do
+ expect(Rails.logger).to receive(:info).with(
+ a_string_matching(described_class.name)
+ .and(matching([start_id, stop_id].inspect))
+ .and(matching(exception.name))
+ )
+
+ perform_ignoring_exceptions
+ end
+
+ it 'still adds diff commits' do
+ expect { perform_ignoring_exceptions }
+ .to change { MergeRequestDiffCommit.count }
+ end
+
+ it 'still adds diff files' do
+ expect { perform_ignoring_exceptions }
+ .to change { MergeRequestDiffFile.count }
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
index 59f69d1e4b1..7b5a00c6111 100644
--- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
end
describe '#perform' do
- it 'renames the path of system-uploads', truncate: true do
+ it 'renames the path of system-uploads', :truncate do
upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
migration.perform('uploads/system/', 'uploads/-/system/')
diff --git a/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb
new file mode 100644
index 00000000000..dfbf1bb681a
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::NormalizeLdapExternUidsRange, :migration, schema: 20170921101004 do
+ let!(:identities) { table(:identities) }
+
+ before do
+ # LDAP identities
+ (1..4).each do |i|
+ identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i)
+ end
+
+ # Non-LDAP identity
+ identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5)
+
+ # Another LDAP identity
+ identities.create!(id: 6, provider: 'ldapmain', extern_uid: " uid = foo 6, ou = People, dc = example, dc = com ", user_id: 6)
+ end
+
+ it 'normalizes the LDAP identities in the range' do
+ described_class.new.perform(1, 3)
+ expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com")
+ expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com")
+ expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com")
+ expect(identities.find(4).extern_uid).to eq(" uid = foo 4, ou = People, dc = example, dc = com ")
+ expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ expect(identities.find(6).extern_uid).to eq(" uid = foo 6, ou = People, dc = example, dc = com ")
+
+ described_class.new.perform(4, 6)
+ expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com")
+ expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com")
+ expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com")
+ expect(identities.find(4).extern_uid).to eq("uid=foo 4,ou=people,dc=example,dc=com")
+ expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ expect(identities.find(6).extern_uid).to eq("uid=foo 6,ou=people,dc=example,dc=com")
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
new file mode 100644
index 00000000000..2c2684a6fc9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
+ let(:migration) { described_class.new }
+ let(:base1) { create(:project) }
+ let(:base1_fork1) { create(:project) }
+ let(:base1_fork2) { create(:project) }
+
+ let(:base2) { create(:project) }
+ let(:base2_fork1) { create(:project) }
+ let(:base2_fork2) { create(:project) }
+
+ let!(:forked_project_links) { table(:forked_project_links) }
+ let!(:fork_networks) { table(:fork_networks) }
+ let!(:fork_network_members) { table(:fork_network_members) }
+
+ let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
+ let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
+
+ before do
+ # A normal fork link
+ forked_project_links.create(id: 1,
+ forked_from_project_id: base1.id,
+ forked_to_project_id: base1_fork1.id)
+ forked_project_links.create(id: 2,
+ forked_from_project_id: base1.id,
+ forked_to_project_id: base1_fork2.id)
+
+ forked_project_links.create(id: 3,
+ forked_from_project_id: base2.id,
+ forked_to_project_id: base2_fork1.id)
+ forked_project_links.create(id: 4,
+ forked_from_project_id: base2_fork1.id,
+ forked_to_project_id: create(:project).id)
+
+ forked_project_links.create(id: 5,
+ forked_from_project_id: base2.id,
+ forked_to_project_id: base2_fork2.id)
+
+ migration.perform(1, 3)
+ end
+
+ it 'it creates the fork network' do
+ expect(fork_network1).not_to be_nil
+ expect(fork_network2).not_to be_nil
+ end
+
+ it 'does not create a fork network for a fork-of-fork' do
+ # perfrom the entire batch
+ migration.perform(1, 5)
+
+ expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil
+ end
+
+ it 'creates memberships for the root of fork networks' do
+ base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1.id)
+ base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2.id)
+
+ expect(base1_membership).not_to be_nil
+ expect(base2_membership).not_to be_nil
+ end
+
+ it 'skips links that had their source project deleted' do
+ forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: create(:project).id)
+
+ migration.perform(5, 8)
+
+ expect(fork_networks.find_by(root_project_id: 99999)).to be_nil
+ end
+
+ it 'schedules a job for inserting memberships for forks-of-forks' do
+ delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3])
+
+ migration.perform(1, 3)
+ end
+
+ it 'only processes a single batch of links at a time' do
+ expect(fork_network_members.count).to eq(5)
+
+ migration.perform(3, 5)
+
+ expect(fork_network_members.count).to eq(7)
+ end
+
+ it 'can be repeated without effect' do
+ expect { migration.perform(1, 3) }.not_to change { fork_network_members.count }
+ end
+end
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index 8772d3d5ada..422f2af7266 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -26,6 +26,9 @@ describe Backup::Manager do
[
'1451606400_2016_01_01_1.2.3_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar',
'1451510000_2015_12_30_gitlab_backup.tar',
'1450742400_2015_12_22_gitlab_backup.tar',
'1449878400_gitlab_backup.tar',
@@ -57,6 +60,30 @@ describe Backup::Manager do
end
end
+ context 'when no valid file is found' do
+ let(:files) do
+ [
+ '14516064000_2016_01_01_1.2.3_gitlab_backup.tar',
+ 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar'
+ ]
+ end
+
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (0 removed)')
+ end
+ end
+
context 'when there are no files older than keep_time' do
before do
# Set to 30 days
@@ -84,16 +111,22 @@ describe Backup::Manager do
it 'removes matching files with a human-readable versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[1])
- end
-
- it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[2])
expect(FileUtils).to have_received(:rm).with(files[3])
end
- it 'removes matching files without a human-readable timestamp' do
+ it 'removes matching files with a human-readable versioned timestamp with tagged EE' do
expect(FileUtils).to have_received(:rm).with(files[4])
+ end
+
+ it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[5])
+ expect(FileUtils).to have_received(:rm).with(files[6])
+ end
+
+ it 'removes matching files without a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[7])
+ expect(FileUtils).to have_received(:rm).with(files[8])
end
it 'does not remove files that are not old enough' do
@@ -101,11 +134,11 @@ describe Backup::Manager do
end
it 'does not remove non-matching files' do
- expect(FileUtils).not_to have_received(:rm).with(files[6])
+ expect(FileUtils).not_to have_received(:rm).with(files[9])
end
it 'prints a done message' do
- expect(progress).to have_received(:puts).with('done. (5 removed)')
+ expect(progress).to have_received(:puts).with('done. (8 removed)')
end
end
@@ -121,14 +154,15 @@ describe Backup::Manager do
end
it 'removes the remaining expected files' do
- expect(FileUtils).to have_received(:rm).with(files[2])
- expect(FileUtils).to have_received(:rm).with(files[3])
expect(FileUtils).to have_received(:rm).with(files[4])
expect(FileUtils).to have_received(:rm).with(files[5])
+ expect(FileUtils).to have_received(:rm).with(files[6])
+ expect(FileUtils).to have_received(:rm).with(files[7])
+ expect(FileUtils).to have_received(:rm).with(files[8])
end
it 'sets the correct removed count' do
- expect(progress).to have_received(:puts).with('done. (4 removed)')
+ expect(progress).to have_received(:puts).with('done. (7 removed)')
end
it 'prints the error from file that could not be removed' do
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index f8c8b83a3ac..633e319f46d 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -3,15 +3,15 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
- context "exit code checking", skip_gitaly_mock: true do
+ context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
- allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
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])
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
new file mode 100644
index 00000000000..33540eab5d6
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -0,0 +1,246 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2html do
+ subject { described_class }
+
+ it "prints non-ansi as-is" do
+ expect(convert_html("Hello")).to eq('Hello')
+ end
+
+ it "strips non-color-changing controll sequences" do
+ expect(convert_html("Hello \e[2Kworld")).to eq('Hello world')
+ end
+
+ it "prints simply red" do
+ expect(convert_html("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply red without trailing reset" do
+ expect(convert_html("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply yellow" do
+ expect(convert_html("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
+ end
+
+ it "prints default on blue" do
+ expect(convert_html("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
+ end
+
+ it "prints red on blue" do
+ expect(convert_html("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
+ end
+
+ it "resets colors after red on blue" do
+ expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
+ end
+
+ it "performs color change from red/blue to yellow/blue" do
+ expect(convert_html("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
+ end
+
+ it "performs color change from red/blue to yellow/green" do
+ expect(convert_html("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
+ end
+
+ it "performs color change from red/blue to reset to yellow/green" do
+ expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
+ end
+
+ it "ignores unsupported codes" do
+ expect(convert_html("\e[51mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints light red" do
+ expect(convert_html("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>')
+ end
+
+ it "prints default on light red" do
+ expect(convert_html("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
+ end
+
+ it "performs color change from red/blue to default/blue" do
+ expect(convert_html("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "performs color change from light red/blue to default/blue" do
+ expect(convert_html("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "prints bold text" do
+ expect(convert_html("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
+ end
+
+ it "resets bold text" do
+ expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
+ expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
+ end
+
+ it "prints italic text" do
+ expect(convert_html("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
+ end
+
+ it "resets italic text" do
+ expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
+ end
+
+ it "prints underlined text" do
+ expect(convert_html("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
+ end
+
+ it "resets underlined text" do
+ expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
+ end
+
+ it "prints concealed text" do
+ expect(convert_html("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
+ end
+
+ it "resets concealed text" do
+ expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
+ end
+
+ it "prints crossed-out text" do
+ expect(convert_html("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
+ end
+
+ it "resets crossed-out text" do
+ expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
+ end
+
+ it "can print 256 xterm fg colors" do
+ expect(convert_html("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
+ end
+
+ it "can print 256 xterm fg colors on normal magenta background" do
+ expect(convert_html("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors" do
+ expect(convert_html("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors on normal magenta foreground" do
+ expect(convert_html("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
+ end
+
+ it "prints bold colored text vividly" do
+ expect(convert_html("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+
+ it "prints bold light colored text correctly" do
+ expect(convert_html("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+
+ it "prints &lt;" do
+ expect(convert_html("<")).to eq('&lt;')
+ end
+
+ it "replaces newlines with line break tags" do
+ expect(convert_html("\n")).to eq('<br>')
+ end
+
+ it "groups carriage returns with newlines" do
+ expect(convert_html("\r\n")).to eq('<br>')
+ end
+
+ describe "incremental update" do
+ shared_examples 'stateable converter' do
+ let(:pass1_stream) { StringIO.new(pre_text) }
+ let(:pass2_stream) { StringIO.new(pre_text + text) }
+ let(:pass1) { subject.convert(pass1_stream) }
+ let(:pass2) { subject.convert(pass2_stream, pass1.state) }
+
+ it "to returns html to append" do
+ expect(pass2.append).to be_truthy
+ expect(pass2.html).to eq(html)
+ expect(pass1.html + pass2.html).to eq(pre_html + html)
+ end
+ end
+
+ context "with split word" do
+ let(:pre_text) { "\e[1mHello" }
+ let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
+ let(:text) { "\e[1mWorld" }
+ let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context "with split sequence" do
+ let(:pre_text) { "\e[1m" }
+ let(:pre_html) { "<span class=\"term-bold\"></span>" }
+ let(:text) { "Hello" }
+ let(:html) { "<span class=\"term-bold\">Hello</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context "with partial sequence" do
+ let(:pre_text) { "Hello\e" }
+ let(:pre_html) { "Hello" }
+ let(:text) { "[1m World" }
+ let(:html) { "<span class=\"term-bold\"> World</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context 'with new line' do
+ let(:pre_text) { "Hello\r" }
+ let(:pre_html) { "Hello\r" }
+ let(:text) { "\nWorld" }
+ let(:html) { "<br>World" }
+
+ it_behaves_like 'stateable converter'
+ end
+ end
+
+ context "with section markers" do
+ let(:section_name) { 'test_section' }
+ let(:section_start_time) { Time.new(2017, 9, 20).utc }
+ let(:section_duration) { 3.seconds }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_start_html) do
+ '<div class="hidden" data-action="start"'\
+ " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\
+ "#{section_start[0...-5]}</div>"
+ end
+ let(:section_end_html) do
+ '<div class="hidden" data-action="end"'\
+ " data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\
+ "#{section_end[0...-5]}</div>"
+ end
+
+ it "prints light red" do
+ text = "#{section_start}\e[91mHello\e[0m\n#{section_end}"
+ html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}}
+
+ expect(convert_html(text)).to eq(html)
+ end
+ end
+
+ describe "truncates" do
+ let(:text) { "Hello World" }
+ let(:stream) { StringIO.new(text) }
+ let(:subject) { described_class.convert(stream) }
+
+ before do
+ stream.seek(3, IO::SEEK_SET)
+ end
+
+ it "returns truncated output" do
+ expect(subject.truncated).to be_truthy
+ end
+
+ it "does not append output" do
+ expect(subject.append).to be_falsey
+ end
+ end
+
+ def convert_html(data)
+ stream = StringIO.new(data)
+ subject.convert(stream).html
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
new file mode 100644
index 00000000000..15eb01eb472
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Kubernetes do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when kubernetes service is active' do
+ set(:project) { create(:kubernetes_project) }
+
+ it 'is satisfied by a kubernetes pipeline' do
+ expect(described_class.new('active'))
+ .to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when kubernetes service is inactive' do
+ set(:project) { create(:project) }
+
+ it 'is not satisfied by a pipeline without kubernetes available' do
+ expect(described_class.new('active'))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when kubernetes policy is invalid' do
+ it 'raises an error' do
+ expect { described_class.new('unknown') }
+ .to raise_error(described_class::UnknownPolicyError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
new file mode 100644
index 00000000000..7211187e511
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Refs do
+ describe '#satisfied_by?' do
+ context 'when matching ref' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'master') }
+
+ it 'is satisfied when pipeline branch matches' do
+ expect(described_class.new(%w[master deploy]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when pipeline branch does not match' do
+ expect(described_class.new(%w[feature fix]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when maching tags' do
+ context 'when pipeline runs for a tag' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'feature', tag: true)
+ end
+
+ it 'is satisfied when tags matcher is specified' do
+ expect(described_class.new(%w[master tags]))
+ .to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when pipeline is not created for a tag' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'feature', tag: false)
+ end
+
+ it 'is not satisfied when tag match is specified' do
+ expect(described_class.new(%w[master tags]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+ end
+
+ context 'when also matching a path' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'master')
+ end
+
+ it 'is satisfied when provided patch matches specified one' do
+ expect(described_class.new(%W[master@#{pipeline.project_full_path}]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when path differs' do
+ expect(described_class.new(%w[master@some/fork/repository]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when maching a source' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) }
+
+ it 'is satisifed when provided source keyword matches' do
+ expect(described_class.new(%w[pushes]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when provided source keyword does not match' do
+ expect(described_class.new(%w[triggers]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when matching a ref by a regular expression' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'docs-something') }
+
+ it 'is satisfied when regexp matches pipeline ref' do
+ expect(described_class.new(['/docs-.*/']))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when regexp does not match pipeline ref' do
+ expect(described_class.new(['/fix-.*/']))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb
new file mode 100644
index 00000000000..20ee3dd3e89
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy do
+ let(:policy) { spy('policy specification') }
+
+ before do
+ stub_const("#{described_class}::Something", policy)
+ end
+
+ describe '.fabricate' do
+ context 'when policy exists' do
+ it 'fabricates and initializes relevant policy' do
+ specs = described_class.fabricate(something: 'some value')
+
+ expect(specs).to be_an Array
+ expect(specs).to be_one
+ expect(policy).to have_received(:new).with('some value')
+ end
+ end
+
+ context 'when some policies are not defined' do
+ it 'gracefully skips unknown policies' do
+ expect { described_class.fabricate(unknown: 'first') }
+ .to raise_error(NameError)
+ end
+ end
+
+ context 'when passing a nil value as specs' do
+ it 'returns an empty array' do
+ specs = described_class.fabricate(nil)
+
+ expect(specs).to be_an Array
+ expect(specs).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb
new file mode 100644
index 00000000000..f8188675013
--- /dev/null
+++ b/spec/lib/gitlab/ci/charts_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Charts do
+ context "pipeline_times" do
+ let(:project) { create(:project) }
+ let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) }
+
+ subject { chart.pipeline_times }
+
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
+ it 'returns pipeline times in minutes' do
+ is_expected.to contain_exactly(2)
+ end
+
+ it 'handles nil pipeline times' do
+ create(:ci_empty_pipeline, project: project, duration: nil)
+
+ is_expected.to contain_exactly(2, 0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb
new file mode 100644
index 00000000000..3789a142248
--- /dev/null
+++ b/spec/lib/gitlab/ci/mask_secret_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::MaskSecret do
+ subject { described_class }
+
+ describe '#mask' do
+ it 'masks exact number of characters' do
+ expect(mask('token', 'oke')).to eq('txxxn')
+ end
+
+ it 'masks multiple occurrences' do
+ expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
+ end
+
+ it 'does not mask if not found' do
+ expect(mask('token', 'not')).to eq('token')
+ end
+
+ it 'does support null token' do
+ expect(mask('token', nil)).to eq('token')
+ end
+
+ def mask(value, token)
+ subject.mask!(value.dup, token)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
new file mode 100644
index 00000000000..f54e2326b06
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Create do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:pipeline) do
+ build(:ci_pipeline_with_one_job, project: project,
+ ref: 'master')
+ end
+
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ seeds_block: nil)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ before do
+ step.perform!
+ end
+
+ context 'when pipeline is ready to be saved' do
+ it 'saves a pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'creates stages' do
+ expect(pipeline.reload.stages).to be_one
+ end
+ end
+
+ context 'when pipeline has validation errors' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: nil)
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'appends validation error' do
+ expect(pipeline.errors.to_a)
+ .to include /Failed to persist the pipeline/
+ end
+ end
+
+ context 'when there is a seed block present' do
+ let(:seeds) { spy('pipeline seeds') }
+
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ seeds_block: seeds)
+ end
+
+ it 'executes the block' do
+ expect(seeds).to have_received(:call).with(pipeline)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
new file mode 100644
index 00000000000..e165e0fac2a
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Sequence do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
+ let(:command) { double('command' ) }
+ let(:first_step) { spy('first step') }
+ let(:second_step) { spy('second step') }
+ let(:sequence) { [first_step, second_step] }
+
+ subject do
+ described_class.new(pipeline, command, sequence)
+ end
+
+ context 'when one of steps breaks the chain' do
+ before do
+ allow(first_step).to receive(:break?).and_return(true)
+ end
+
+ it 'does not process the second step' do
+ subject.build! do |pipeline, sequence|
+ expect(sequence).not_to be_complete
+ end
+
+ expect(second_step).not_to have_received(:perform!)
+ end
+
+ it 'returns a pipeline object' do
+ expect(subject.build!).to eq pipeline
+ end
+ end
+
+ context 'when all chains are executed correctly' do
+ before do
+ sequence.each do |step|
+ allow(step).to receive(:break?).and_return(false)
+ end
+ end
+
+ it 'iterates through entire sequence' do
+ subject.build! do |pipeline, sequence|
+ expect(sequence).to be_complete
+ end
+
+ expect(first_step).to have_received(:perform!)
+ expect(second_step).to have_received(:perform!)
+ end
+
+ it 'returns a pipeline object' do
+ expect(subject.build!).to eq pipeline
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
new file mode 100644
index 00000000000..32bd5de829b
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Skip do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ ignore_skip_ci: false,
+ save_incompleted: true)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ context 'when pipeline has been skipped by a user' do
+ before do
+ allow(pipeline).to receive(:git_commit_message)
+ .and_return('commit message [ci skip]')
+
+ step.perform!
+ end
+
+ it 'should break the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'skips the pipeline' do
+ expect(pipeline.reload).to be_skipped
+ end
+ end
+
+ context 'when pipeline has not been skipped' do
+ before do
+ step.perform!
+ end
+
+ it 'should not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'should not skip a pipeline chain' do
+ expect(pipeline.reload).not_to be_skipped
+ end
+ end
+
+ context 'when [ci skip] should be ignored' do
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ ignore_skip_ci: true)
+ end
+
+ it 'does not break the chain' do
+ step.perform!
+
+ expect(step.break?).to be false
+ end
+ end
+
+ context 'when pipeline should be skipped but not persisted' do
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ ignore_skip_ci: false,
+ save_incompleted: false)
+ end
+
+ before do
+ allow(pipeline).to receive(:git_commit_message)
+ .and_return('commit message [ci skip]')
+
+ step.perform!
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'does not skip pipeline' do
+ expect(pipeline.reload).not_to be_skipped
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
new file mode 100644
index 00000000000..0bbdd23f4d6
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: ref, project: project)
+ end
+
+ let(:command) do
+ double('command', project: project, current_user: user)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ let(:ref) { 'master' }
+
+ context 'when users has no ability to run a pipeline' do
+ before do
+ step.perform!
+ end
+
+ it 'adds an error about insufficient permissions' do
+ expect(pipeline.errors.to_a)
+ .to include /Insufficient permissions/
+ end
+
+ it 'breaks the pipeline builder chain' do
+ expect(step.break?).to eq true
+ end
+ end
+
+ context 'when user has ability to create a pipeline' do
+ before do
+ project.add_developer(user)
+
+ step.perform!
+ end
+
+ it 'does not invalidate the pipeline' do
+ expect(pipeline).to be_valid
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to eq false
+ end
+ end
+
+ describe '#allowed_to_create?' do
+ subject { step.allowed_to_create? }
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the branch is protected' do
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when developers are allowed to merge' do
+ let!(:protected_branch) do
+ create(:protected_branch,
+ :developers_can_merge,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when the tag is protected' do
+ let(:ref) { 'v1.0.0' }
+
+ let!(:protected_tag) do
+ create(:protected_tag, project: project, name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when developers are allowed to create the tag' do
+ let!(:protected_tag) do
+ create(:protected_tag,
+ :developers_can_create,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the branch is protected' do
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the tag is protected' do
+ let(:ref) { 'v1.0.0' }
+
+ let!(:protected_tag) do
+ create(:protected_tag, project: project, name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when no one can create the tag' do
+ let!(:protected_tag) do
+ create(:protected_tag,
+ :no_one_can_create,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'when owner cannot create pipeline' do
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
new file mode 100644
index 00000000000..8357af38f92
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ save_incompleted: true)
+ end
+
+ let!(:step) { described_class.new(pipeline, command) }
+
+ before do
+ step.perform!
+ end
+
+ context 'when pipeline has no YAML configuration' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, project: project)
+ end
+
+ it 'appends errors about missing configuration' do
+ expect(pipeline.errors.to_a)
+ .to include 'Missing .gitlab-ci.yml file'
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+ end
+
+ context 'when YAML configuration contains errors' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, config: 'invalid YAML')
+ end
+
+ it 'appends errors about YAML errors' do
+ expect(pipeline.errors.to_a)
+ .to include 'Invalid configuration format'
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ context 'when saving incomplete pipeline is allowed' do
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ save_incompleted: true)
+ end
+
+ it 'fails the pipeline' do
+ expect(pipeline.reload).to be_failed
+ end
+
+ it 'sets a config error failure reason' do
+ expect(pipeline.reload.config_error?).to eq true
+ end
+ end
+
+ context 'when saving incomplete pipeline is not allowed' do
+ let(:command) do
+ double('command', project: project,
+ current_user: user,
+ save_incompleted: false)
+ end
+
+ it 'does not drop pipeline' do
+ expect(pipeline).not_to be_failed
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+
+ context 'when pipeline has no stages / jobs' do
+ let(:config) do
+ { rspec: {
+ script: 'ls',
+ only: ['something']
+ } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, config: config)
+ end
+
+ it 'appends an error about missing stages' do
+ expect(pipeline.errors.to_a)
+ .to include 'No stages / jobs for this pipeline.'
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+ end
+
+ context 'when pipeline contains configuration validation errors' do
+ let(:config) { { rspec: {} } }
+
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, config: config)
+ end
+
+ it 'appends configuration validation errors to pipeline errors' do
+ expect(pipeline.errors.to_a)
+ .to include "jobs:rspec config can't be blank"
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+ end
+
+ context 'when pipeline is correct and complete' do
+ let(:pipeline) do
+ build(:ci_pipeline_with_one_job, project: project)
+ end
+
+ it 'does not invalidate the pipeline' do
+ expect(pipeline).to be_valid
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
new file mode 100644
index 00000000000..bb356efe9ad
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ let(:command) do
+ double('command', project: project, current_user: user)
+ end
+
+ let!(:step) { described_class.new(pipeline, command) }
+
+ before do
+ step.perform!
+ end
+
+ context 'when pipeline ref and sha exists' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project)
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'does not append pipeline errors' do
+ expect(pipeline.errors).to be_empty
+ end
+ end
+
+ context 'when pipeline ref does not exist' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'something', project: project)
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'adds an error about missing ref' do
+ expect(pipeline.errors.to_a)
+ .to include 'Reference not found'
+ end
+ end
+
+ context 'when pipeline does not have SHA set' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project)
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'adds an error about missing SHA' do
+ expect(pipeline.errors.to_a)
+ .to include 'Commit not found'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
new file mode 100644
index 00000000000..7c9836e2da6
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Duration do
+ let(:calculated_duration) { calculate(data) }
+
+ shared_examples 'calculating duration' do
+ it do
+ expect(calculated_duration).to eq(duration)
+ end
+ end
+
+ context 'test sample A' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [3, 4],
+ [5, 6]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample B' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [0, 4]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample C' do
+ let(:data) do
+ [[0, 4],
+ [2, 6],
+ [5, 7],
+ [8, 9]]
+ end
+
+ let(:duration) { 8 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample D' do
+ let(:data) do
+ [[0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample E' do
+ let(:data) do
+ [[0, 1],
+ [3, 9],
+ [3, 4],
+ [3, 5],
+ [3, 8],
+ [4, 5],
+ [4, 7],
+ [5, 8]]
+ end
+
+ let(:duration) { 7 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample F' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [2, 4],
+ [2, 4],
+ [5, 8]]
+ end
+
+ let(:duration) { 6 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample G' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ def calculate(data)
+ periods = data.shuffle.map do |(first, last)|
+ described_class::Period.new(first, last)
+ end
+
+ described_class.from_periods(periods.sort_by(&:first))
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
deleted file mode 100644
index b26728a843c..00000000000
--- a/spec/lib/gitlab/ci/pipeline_duration_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::PipelineDuration do
- let(:calculated_duration) { calculate(data) }
-
- shared_examples 'calculating duration' do
- it do
- expect(calculated_duration).to eq(duration)
- end
- end
-
- context 'test sample A' do
- let(:data) do
- [[0, 1],
- [1, 2],
- [3, 4],
- [5, 6]]
- end
-
- let(:duration) { 4 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample B' do
- let(:data) do
- [[0, 1],
- [1, 2],
- [2, 3],
- [3, 4],
- [0, 4]]
- end
-
- let(:duration) { 4 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample C' do
- let(:data) do
- [[0, 4],
- [2, 6],
- [5, 7],
- [8, 9]]
- end
-
- let(:duration) { 8 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample D' do
- let(:data) do
- [[0, 1],
- [2, 3],
- [4, 5],
- [6, 7]]
- end
-
- let(:duration) { 4 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample E' do
- let(:data) do
- [[0, 1],
- [3, 9],
- [3, 4],
- [3, 5],
- [3, 8],
- [4, 5],
- [4, 7],
- [5, 8]]
- end
-
- let(:duration) { 7 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample F' do
- let(:data) do
- [[1, 3],
- [2, 4],
- [2, 4],
- [2, 4],
- [5, 8]]
- end
-
- let(:duration) { 6 }
-
- it_behaves_like 'calculating duration'
- end
-
- context 'test sample G' do
- let(:data) do
- [[1, 3],
- [2, 4],
- [6, 7]]
- end
-
- let(:duration) { 4 }
-
- it_behaves_like 'calculating duration'
- end
-
- def calculate(data)
- periods = data.shuffle.map do |(first, last)|
- Gitlab::Ci::PipelineDuration::Period.new(first, last)
- end
-
- Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
- end
-end
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
index 9ecd128faca..3fe8d50c49a 100644
--- a/spec/lib/gitlab/ci/stage/seed_spec.rb
+++ b/spec/lib/gitlab/ci/stage/seed_spec.rb
@@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do
described_class.new(pipeline, 'test', builds)
end
+ describe '#size' do
+ it 'returns a number of jobs in the stage' do
+ expect(subject.size).to eq 2
+ end
+ end
+
describe '#stage' do
it 'returns hash attributes of a stage' do
expect(subject.stage).to be_a Hash
diff --git a/spec/lib/gitlab/ci/trace/section_parser_spec.rb b/spec/lib/gitlab/ci/trace/section_parser_spec.rb
new file mode 100644
index 00000000000..ca53ff87c6f
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/section_parser_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::SectionParser do
+ def lines_with_pos(text)
+ pos = 0
+ StringIO.new(text).each_line do |line|
+ yield line, pos
+ pos += line.bytesize + 1 # newline
+ end
+ end
+
+ def build_lines(text)
+ to_enum(:lines_with_pos, text)
+ end
+
+ def section(name, start, duration, text)
+ end_ = start + duration
+ "section_start:#{start.to_i}:#{name}\r\033[0K#{text}section_end:#{end_.to_i}:#{name}\r\033[0K"
+ end
+
+ let(:lines) { build_lines('') }
+ subject { described_class.new(lines) }
+
+ describe '#sections' do
+ before do
+ subject.parse!
+ end
+
+ context 'empty trace' do
+ let(:lines) { build_lines('') }
+
+ it { expect(subject.sections).to be_empty }
+ end
+
+ context 'with a sectionless trace' do
+ let(:lines) { build_lines("line 1\nline 2\n") }
+
+ it { expect(subject.sections).to be_empty }
+ end
+
+ context 'with trace markers' do
+ let(:start_time) { Time.new(2017, 10, 5).utc }
+ let(:section_b_duration) { 1.second }
+ let(:section_a) { section('a', start_time, 0, 'a line') }
+ let(:section_b) { section('b', start_time, section_b_duration, "another line\n") }
+ let(:lines) { build_lines(section_a + section_b) }
+
+ it { expect(subject.sections.size).to eq(2) }
+ it { expect(subject.sections[1][:name]).to eq('b') }
+ it { expect(subject.sections[1][:date_start]).to eq(start_time) }
+ it { expect(subject.sections[1][:date_end]).to eq(start_time + section_b_duration) }
+ end
+ end
+
+ describe '#parse!' do
+ context 'multiple "section_" but no complete markers' do
+ let(:lines) { build_lines('section_section_section_') }
+
+ it 'must find 3 possible section start but no complete sections' do
+ expect(subject).to receive(:find_next_marker).exactly(3).times.and_call_original
+
+ subject.parse!
+
+ expect(subject.sections).to be_empty
+ end
+ end
+
+ context 'trace with UTF-8 chars' do
+ let(:line) { 'GitLab â¤ï¸ 狸 (tanukis)\n' }
+ let(:trace) { section('test_section', Time.new(2017, 10, 5).utc, 3.seconds, line) }
+ let(:lines) { build_lines(trace) }
+
+ it 'must handle correctly byte positioning' do
+ expect(subject).to receive(:find_next_marker).exactly(2).times.and_call_original
+
+ subject.parse!
+
+ sections = subject.sections
+
+ expect(sections.size).to eq(1)
+ s = sections[0]
+ len = s[:byte_end] - s[:byte_start]
+ expect(trace.byteslice(s[:byte_start], len)).to eq(line)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 9cb0b62590a..3546532b9b4 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -61,6 +61,93 @@ describe Gitlab::Ci::Trace do
end
end
+ describe '#extract_sections' do
+ let(:log) { 'No sections' }
+ let(:sections) { trace.extract_sections }
+
+ before do
+ trace.set(log)
+ end
+
+ context 'no sections' do
+ it 'returs []' do
+ expect(trace.extract_sections).to eq([])
+ end
+ end
+
+ context 'multiple sections available' do
+ let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
+ let(:sections_data) do
+ [
+ { name: 'prepare_script', lines: 2, duration: 3.seconds },
+ { name: 'get_sources', lines: 4, duration: 1.second },
+ { name: 'restore_cache', lines: 0, duration: 0.seconds },
+ { name: 'download_artifacts', lines: 0, duration: 0.seconds },
+ { name: 'build_script', lines: 2, duration: 1.second },
+ { name: 'after_script', lines: 0, duration: 0.seconds },
+ { name: 'archive_cache', lines: 0, duration: 0.seconds },
+ { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
+ ]
+ end
+
+ it "returns valid sections" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(sections_data.size),
+ "expected #{sections_data.size} sections, got #{sections.size}"
+
+ buff = StringIO.new(log)
+ sections.each_with_index do |s, i|
+ expected = sections_data[i]
+
+ expect(s[:name]).to eq(expected[:name])
+ expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
+
+ buff.seek(s[:byte_start], IO::SEEK_SET)
+ length = s[:byte_end] - s[:byte_start]
+ lines = buff.read(length).count("\n")
+ expect(lines).to eq(expected[:lines])
+ end
+ end
+ end
+
+ context 'logs contains "section_start"' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
+
+ it "returns only one section" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(1)
+
+ section = sections[0]
+ expect(section[:name]).to eq('a_section')
+ expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
+ end
+ end
+
+ context 'missing section_end' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'missing section_start' do
+ let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'inverted section_start section_end' do
+ let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+ end
+
describe '#set' do
before do
trace.set("12")
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
new file mode 100644
index 00000000000..d72f8553f55
--- /dev/null
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -0,0 +1,1716 @@
+require 'spec_helper'
+
+module Gitlab
+ module Ci
+ describe YamlProcessor, :lib do
+ subject { described_class.new(config) }
+
+ describe 'our current .gitlab-ci.yml' do
+ let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
+
+ it 'is valid' do
+ error_message = described_class.validation_message(config)
+
+ expect(error_message).to be_nil
+ end
+ end
+
+ describe '#build_attributes' do
+ subject { described_class.new(config).build_attributes(:rspec) }
+
+ describe 'coverage entry' do
+ describe 'code coverage regexp' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ coverage: '/Code coverage: \d+\.\d+/' })
+ end
+
+ it 'includes coverage regexp in build attributes' do
+ expect(subject)
+ .to include(coverage_regex: 'Code coverage: \d+\.\d+')
+ end
+ end
+ end
+
+ describe 'retry entry' do
+ context 'when retry count is specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec', retry: 1 })
+ end
+
+ it 'includes retry count in build options attribute' do
+ expect(subject[:options]).to include(retry: 1)
+ end
+ end
+
+ context 'when retry count is not specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'does not persist retry count in the database' do
+ expect(subject[:options]).not_to have_key(:retry)
+ end
+ end
+ end
+
+ describe 'allow failure entry' do
+ context 'when job is a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual' })
+ end
+
+ it 'is allowed to fail' do
+ expect(subject[:allow_failure]).to be true
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe '#stage_seeds' do
+ context 'when no refs policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
+ rspec: { stage: 'test', script: 'rspec' },
+ spinach: { stage: 'test', script: 'spinach' })
+ end
+
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'correctly fabricates a stage seeds object' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.second.stage[:name]).to eq 'deploy'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
+ expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
+
+ context 'when refs policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
+ end
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: 'feature', tag: true)
+ end
+
+ it 'returns stage seeds only assigned to master to master' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when source policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
+ end
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, source: :schedule)
+ end
+
+ it 'returns stage seeds only assigned to schedules' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when kubernetes policy is specified' do
+ let(:config) do
+ YAML.dump(
+ spinach: { stage: 'test', script: 'spinach' },
+ production: {
+ stage: 'deploy',
+ script: 'cap',
+ only: { kubernetes: 'active' }
+ }
+ )
+ end
+
+ context 'when kubernetes is active' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
+
+ context 'when kubernetes is not active' do
+ it 'does not return seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+ end
+ end
+
+ describe "#pipeline_stage_builds" do
+ let(:type) { 'test' }
+
+ it "returns builds if no branch specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ describe 'only' do
+ it "does not return builds if only has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
+ end
+
+ it "does not return builds if only has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["/^deploy$/"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
+ end
+
+ it "returns builds if only has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["master"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
+ end
+
+ it "returns builds if only has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: %w(master deploy) }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
+ end
+
+ it "returns builds if only has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
+ end
+
+ it "does not return builds if only has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["tags"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
+ end
+
+ it "returns builds if only has special keywords specified and source matches" do
+ possibilities = [{ keyword: 'pushes', source: 'push' },
+ { keyword: 'web', source: 'web' },
+ { keyword: 'triggers', source: 'trigger' },
+ { keyword: 'schedules', source: 'schedule' },
+ { keyword: 'api', source: 'api' },
+ { keyword: 'external', source: 'external' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ end
+ end
+
+ it "does not return builds if only has special keywords specified and source doesn't match" do
+ possibilities = [{ keyword: 'pushes', source: 'web' },
+ { keyword: 'web', source: 'push' },
+ { keyword: 'triggers', source: 'schedule' },
+ { keyword: 'schedules', source: 'external' },
+ { keyword: 'api', source: 'trigger' },
+ { keyword: 'external', source: 'api' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ end
+ end
+
+ it "returns builds if only has current repository path" do
+ seed_pipeline = pipeline(ref: 'deploy')
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: {
+ script: "rspec",
+ type: type,
+ only: ["branches@#{seed_pipeline.project_full_path}"]
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1)
+ end
+
+ it "does not return builds if only has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@fork"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
+ end
+
+ it "returns build only for specified type" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", only: %w(master deploy) },
+ staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1)
+ end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'when it is integer' do
+ let(:only) { 1 }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only has to be either an array of conditions or a hash')
+ end
+ end
+
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
+ end
+ end
+ end
+ end
+
+ describe 'except' do
+ it "returns builds if except has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
+ end
+
+ it "returns builds if except has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["/^deploy$/"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
+ end
+
+ it "does not return builds if except has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["master"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
+ end
+
+ it "does not return builds if except has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: %w(master deploy) }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
+ end
+
+ it "does not return builds if except has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
+ end
+
+ it "returns builds if except has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["tags"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
+ end
+
+ it "does not return builds if except has special keywords specified and source matches" do
+ possibilities = [{ keyword: 'pushes', source: 'push' },
+ { keyword: 'web', source: 'web' },
+ { keyword: 'triggers', source: 'trigger' },
+ { keyword: 'schedules', source: 'schedule' },
+ { keyword: 'api', source: 'api' },
+ { keyword: 'external', source: 'external' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ end
+ end
+
+ it "returns builds if except has special keywords specified and source doesn't match" do
+ possibilities = [{ keyword: 'pushes', source: 'web' },
+ { keyword: 'web', source: 'push' },
+ { keyword: 'triggers', source: 'schedule' },
+ { keyword: 'schedules', source: 'external' },
+ { keyword: 'api', source: 'trigger' },
+ { keyword: 'external', source: 'api' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ end
+ end
+
+ it "does not return builds if except has current repository path" do
+ seed_pipeline = pipeline(ref: 'deploy')
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: {
+ script: "rspec",
+ type: type,
+ except: ["branches@#{seed_pipeline.project_full_path}"]
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0)
+ end
+
+ it "returns builds if except has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@fork"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
+ end
+
+ it "returns build except specified type" do
+ master_pipeline = pipeline(ref: 'master')
+ test_pipeline = pipeline(ref: 'test')
+ deploy_pipeline = pipeline(ref: 'deploy')
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] },
+ staging: { script: "deploy", type: "deploy", except: ["master"] },
+ production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2)
+ expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0)
+ expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0)
+ end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'when it is integer' do
+ let(:except) { 1 }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except has to be either an array of conditions or a hash')
+ end
+ end
+
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
+ end
+ end
+ end
+ end
+ end
+
+ describe "Scripts handling" do
+ let(:config_data) { YAML.dump(config) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) }
+
+ subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first }
+
+ describe "before_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("global script\nscript")
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { before_script: ["local script"], script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("local script\nscript")
+ end
+ end
+ end
+
+ describe "script" do
+ let(:config) do
+ {
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("script")
+ end
+ end
+
+ describe "after_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ after_script: ["after_script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["after_script"])
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ after_script: ["local after_script"],
+ test: { after_script: ["local after_script"], script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["local after_script"])
+ end
+ end
+ end
+ end
+
+ describe "Image and service handling" do
+ context "when extended docker configuration is used" do
+ it "returns image and service when defined" do
+ config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: ["mysql", { name: "docker:dind", alias: "docker",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }],
+ before_script: ["pwd"],
+ rspec: { script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "mysql" },
+ { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ it "returns image and service when overridden for job" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
+ script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] },
+ { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context "when etended docker configuration is not used" do
+ it "returns image and service when defined" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql", "docker:dind"],
+ before_script: ["pwd"],
+ rspec: { script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1" },
+ services: [{ name: "mysql" }, { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ it "returns image and service when overridden for job" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.5" },
+ services: [{ name: "postgresql" }, { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+ end
+
+ describe 'Variables' do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ subject { config_processor.builds.first[:yaml_variables] }
+
+ context 'when global variables are defined' do
+ let(:variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+ let(:config) do
+ {
+ variables: variables,
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'returns global variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when job and global variables are defined' do
+ let(:global_variables) do
+ { 'VAR1' => 'global1', 'VAR3' => 'global3' }
+ end
+ let(:job_variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ variables: global_variables,
+ rspec: { script: 'rspec', variables: job_variables }
+ }
+ end
+
+ it 'returns all unique variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR3', value: 'global3', public: true },
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when job variables are defined' do
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec', variables: variables }
+ }
+ end
+
+ context 'when syntax is correct' do
+ let(:variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+
+ it 'returns job variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when syntax is incorrect' do
+ context 'when variables defined but invalid' do
+ let(:variables) do
+ %w(VAR1 value1 VAR2 value2)
+ end
+
+ it 'raises error' do
+ expect { subject }
+ .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ /jobs:rspec:variables config should be a hash of key value pairs/)
+ end
+ end
+
+ context 'when variables key defined but value not specified' do
+ let(:variables) do
+ nil
+ end
+
+ it 'returns empty array' do
+ ##
+ # When variables config is empty, we assume this is a valid
+ # configuration, see issue #18775
+ #
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when job variables are not defined' do
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'returns empty array' do
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ describe "When" do
+ %w(on_success on_failure always).each do |when_state|
+ it "returns #{when_state} when defined" do
+ config = YAML.dump({
+ rspec: { script: "rspec", when: when_state }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
+ expect(builds.size).to eq(1)
+ expect(builds.first[:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe 'cache' do
+ context 'when cache definition has unknown keys' do
+ it 'raises relevant validation error' do
+ config = YAML.dump(
+ { cache: { untracked: true, invalid: 'key' },
+ rspec: { script: 'rspec' } })
+
+ expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error(
+ Gitlab::Ci::YamlProcessor::ValidationError,
+ 'cache config contains unknown keys: invalid'
+ )
+ end
+ end
+
+ it "returns cache when defined globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
+ rspec: {
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ key: 'key',
+ policy: 'pull-push'
+ )
+ end
+
+ it "returns cache when defined in a job" do
+ config = YAML.dump({
+ rspec: {
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ key: 'key',
+ policy: 'pull-push'
+ )
+ end
+
+ it "overwrite cache when defined for a job and globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
+ rspec: {
+ script: "rspec",
+ cache: { paths: ["test/"], untracked: false, key: 'local' }
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ paths: ["test/"],
+ untracked: false,
+ key: 'local',
+ policy: 'pull-push'
+ )
+ end
+ end
+
+ describe "Artifacts" do
+ it "returns artifacts when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: {
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ name: "custom_name",
+ expire_in: "7d"
+ },
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1" },
+ services: [{ name: "mysql" }],
+ artifacts: {
+ name: "custom_name",
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ expire_in: "7d"
+ }
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ %w[on_success on_failure always].each do |when_state|
+ it "returns artifacts for when #{when_state} defined" do
+ config = YAML.dump({
+ rspec: {
+ script: "rspec",
+ artifacts: { paths: ["logs/", "binaries/"], when: when_state }
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
+ expect(builds.size).to eq(1)
+ expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe '#environment' do
+ let(:config) do
+ {
+ deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
+ }
+ end
+
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+ let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) }
+
+ context 'when a production environment is specified' do
+ let(:environment) { 'production' }
+
+ it 'does return production' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment)
+ expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
+ end
+ end
+
+ context 'when hash is specified' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com' }
+ end
+
+ it 'does return production and URL' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+
+ context 'the url has a port as variable' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com:$PORT' }
+ end
+
+ it 'allows a variable for the port' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+ end
+ end
+
+ context 'when no environment is specified' do
+ let(:environment) { nil }
+
+ it 'does return nil environment' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to be_nil
+ end
+ end
+
+ context 'is not a string' do
+ let(:environment) { 1 }
+
+ it 'raises error' do
+ expect { builds }.to raise_error(
+ 'jobs:deploy_to_production:environment config should be a hash or a string')
+ end
+ end
+
+ context 'is not a valid string' do
+ let(:environment) { 'production:staging' }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
+
+ context 'when on_stop is specified' do
+ let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
+ let(:config) { { review: review, close_review: close_review }.compact }
+
+ context 'with matching job' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
+
+ it 'does return a list of builds' do
+ expect(builds.size).to eq(2)
+ expect(builds.first[:environment]).to eq('review')
+ end
+ end
+
+ context 'without matching job' do
+ let(:close_review) { nil }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
+ end
+ end
+
+ context 'with close job without environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
+ end
+ end
+
+ context 'with close job for different environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
+ end
+ end
+
+ context 'with close job without stop action' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
+ end
+ end
+ end
+ end
+
+ describe "Dependencies" do
+ let(:config) do
+ {
+ build1: { stage: 'build', script: 'test' },
+ build2: { stage: 'build', script: 'test' },
+ test1: { stage: 'test', script: 'test', dependencies: dependencies },
+ test2: { stage: 'test', script: 'test' },
+ deploy: { stage: 'test', script: 'test' }
+ }
+ end
+
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'no dependencies' do
+ let(:dependencies) { }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'dependencies to builds' do
+ let(:dependencies) { %w(build1 build2) }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'dependencies to builds defined as symbols' do
+ let(:dependencies) { [:build1, :build2] }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'undefined dependency' do
+ let(:dependencies) { ['undefined'] }
+
+ it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
+ end
+
+ context 'dependencies to deploy' do
+ let(:dependencies) { ['deploy'] }
+
+ it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ end
+ end
+
+ describe "Hidden jobs" do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
+ subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) }
+
+ shared_examples 'hidden_job_handling' do
+ it "doesn't create jobs that start with dot" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "normal_job",
+ commands: "test",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["test"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context 'when hidden job have a script definition' do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { image: 'ruby:2.1', script: 'test' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ it_behaves_like 'hidden_job_handling'
+ end
+
+ context "when hidden job doesn't have a script definition" do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { image: 'ruby:2.1' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ it_behaves_like 'hidden_job_handling'
+ end
+ end
+
+ describe "YAML Alias/Anchor" do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
+ subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) }
+
+ shared_examples 'job_templates_handling' do
+ it "is correctly supported for jobs" do
+ expect(subject.size).to eq(2)
+ expect(subject.first).to eq({
+ stage: "build",
+ stage_idx: 0,
+ name: "job1",
+ commands: "execute-script-for-job",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["execute-script-for-job"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ expect(subject.second).to eq({
+ stage: "build",
+ stage_idx: 0,
+ name: "job2",
+ commands: "execute-script-for-job",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["execute-script-for-job"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context 'when template is a job' do
+ let(:config) do
+ <<EOT
+job1: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
+
+job2: *JOBTMPL
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+
+ context 'when template is a hidden job' do
+ let(:config) do
+ <<EOT
+.template: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
+
+job1: *JOBTMPL
+
+job2: *JOBTMPL
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+
+ context 'when job adds its own keys to a template definition' do
+ let(:config) do
+ <<EOT
+.template: &JOBTMPL
+ stage: build
+
+job1:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+
+job2:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+ end
+
+ describe "Error handling" do
+ it "fails to parse YAML" do
+ expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
+ end
+
+ it "indicates that object is invalid" do
+ expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ end
+
+ it "returns errors if tags parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
+ end
+
+ it "returns errors if before_script parameter is invalid" do
+ config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings")
+ end
+
+ it "returns errors if job before_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
+ end
+
+ it "returns errors if after_script parameter is invalid" do
+ config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings")
+ end
+
+ it "returns errors if job after_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
+ end
+
+ it "returns errors if image parameter is invalid" do
+ config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string")
+ end
+
+ it "returns errors if job name is blank" do
+ config = YAML.dump({ '' => { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank")
+ end
+
+ it "returns errors if job name is non-string" do
+ config = YAML.dump({ 10 => { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol")
+ end
+
+ it "returns errors if job image parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
+ end
+
+ it "returns errors if services parameter is not an array" do
+ config = YAML.dump({ services: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array")
+ end
+
+ it "returns errors if services parameter is not an array of strings" do
+ config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end
+
+ it "returns errors if job services parameter is not an array" do
+ config = YAML.dump({ rspec: { script: "test", services: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
+ end
+
+ it "returns errors if job services parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end
+
+ it "returns error if job configuration is invalid" do
+ config = YAML.dump({ extra: "bundle update" })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash")
+ end
+
+ it "returns errors if services configuration is not correct" do
+ config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array")
+ end
+
+ it "returns errors if there are no jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"] })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if there are no visible jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if job allow_failure parameter is not an boolean" do
+ config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
+ end
+
+ it "returns errors if job stage is not a string" do
+ config = YAML.dump({ rspec: { script: "test", type: 1 } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
+ end
+
+ it "returns errors if job stage is not a pre-defined stage" do
+ config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end
+
+ it "returns errors if job stage is not a defined stage" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+ end
+
+ it "returns errors if stages is not an array" do
+ config = YAML.dump({ stages: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ end
+
+ it "returns errors if stages is not an array of strings" do
+ config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ end
+
+ it "returns errors if variables is not a map" do
+ config = YAML.dump({ variables: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ end
+
+ it "returns errors if variables is not a map of key-value strings" do
+ config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ end
+
+ it "returns errors if job when is not on_success, on_failure or always" do
+ config = YAML.dump({ rspec: { script: "test", when: 1 } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
+ end
+
+ it "returns errors if job artifacts:name is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
+ end
+
+ it "returns errors if job artifacts:when is not an a predefined value" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a valid duration" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ end
+
+ it "returns errors if job artifacts:untracked is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
+ end
+
+ it "returns errors if job artifacts:paths is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
+ end
+
+ it "returns errors if cache:untracked is not an array of strings" do
+ config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
+ end
+
+ it "returns errors if cache:paths is not an array of strings" do
+ config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings")
+ end
+
+ it "returns errors if cache:key is not a string" do
+ config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol")
+ end
+
+ it "returns errors if job cache:key is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
+ end
+
+ it "returns errors if job cache:untracked is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
+ end
+
+ it "returns errors if job cache:paths is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
+ end
+
+ it "returns errors if job dependencies is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
+ end
+ end
+
+ describe "Validate configuration templates" do
+ templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
+
+ templates.each do |file|
+ it "does not return errors for #{file}" do
+ file = File.read(file)
+
+ expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#validation_message" do
+ context "when the YAML could not be parsed" do
+ it "returns an error about invalid configutaion" do
+ content = YAML.dump("invalid: yaml: test")
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content))
+ .to eq "Invalid configuration format"
+ end
+ end
+
+ context "when the tags parameter is invalid" do
+ it "returns an error about invalid tags" do
+ content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content))
+ .to eq "jobs:rspec tags should be an array of strings"
+ end
+ end
+
+ context "when YAML content is empty" do
+ it "returns an error about missing content" do
+ expect(Gitlab::Ci::YamlProcessor.validation_message(''))
+ .to eq "Please provide content of .gitlab-ci.yml"
+ end
+ end
+
+ context "when the YAML is valid" do
+ it "does not return any errors" do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil
+ end
+ end
+ end
+
+ def pipeline(**attributes)
+ build_stubbed(:ci_empty_pipeline, **attributes)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 15012495247..ef7d766a13d 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -254,6 +254,46 @@ describe Gitlab::ClosingIssueExtractor do
expect(subject.closed_by_message(message)).to eq([issue])
end
+ it do
+ message = "Implement: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "Implements: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "Implemented: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "Implementing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "implement: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "implements: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "implemented: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
+ message = "implementing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
@@ -347,10 +387,10 @@ describe Gitlab::ClosingIssueExtractor do
end
it "fetches cross-project URL references" do
- message = "Closes #{urls.project_issue_url(issue2.project, issue2)} and #{reference}"
+ message = "Closes #{urls.project_issue_url(issue2.project, issue2)}, #{reference} and #{urls.project_issue_url(other_issue.project, other_issue)}"
expect(subject.closed_by_message(message))
- .to match_array([issue, issue2])
+ .to match_array([issue, issue2, other_issue])
end
it "ignores invalid cross-project URL references" do
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index cb430b47463..befdc18d1aa 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -47,7 +47,7 @@ describe Gitlab::DataBuilder::Push do
include_examples 'deprecated repository hook data'
it 'does not raise an error when given nil commits' do
- expect { described_class.build(spy, spy, spy, spy, spy, nil) }
+ expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) }
.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1bcdc369c44..3c8350b3aad 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do
.to raise_error(RuntimeError, /Your database user is not allowed/)
end
end
+
+ describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with enough rows to bulk queue jobs more than once' do
+ before do
+ stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1)
+ end
+
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in groups of buffer size 1' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]])
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'with not enough rows to bulk queue jobs more than once' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in bulk all at once (big buffer size)' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]],
+ ['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'without specifying batch_size' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob')
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob')
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
+
+ describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ end
+ end
+ end
+
+ context 'without batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 90aa4f63dd5..596cc435bd9 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -229,7 +229,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#track_rename', redis: true do
+ describe '#track_rename', :redis do
it 'tracks a rename in redis' do
key = 'rename:FakeRenameReservedPathMigrationV1:namespace'
@@ -246,7 +246,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#reverts_for_type', redis: true do
+ describe '#reverts_for_type', :redis do
it 'yields for each tracked rename' do
subject.track_rename('project', 'old_path', 'new_path')
subject.track_rename('project', 'old_path2', 'new_path2')
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index 32ac0b88a9b..1143182531f 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -241,7 +241,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
project = create(:project, :repository, path: 'a-project', namespace: namespace)
subject.rename_namespace(namespace)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 595e06a9748..8922370b0a0 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -115,7 +115,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
subject.rename_project(project)
diff --git a/spec/lib/gitlab/diff/diff_refs_spec.rb b/spec/lib/gitlab/diff/diff_refs_spec.rb
index c73708d90a8..f9bfb4c469e 100644
--- a/spec/lib/gitlab/diff/diff_refs_spec.rb
+++ b/spec/lib/gitlab/diff/diff_refs_spec.rb
@@ -3,6 +3,61 @@ require 'spec_helper'
describe Gitlab::Diff::DiffRefs do
let(:project) { create(:project, :repository) }
+ describe '#==' do
+ let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
+ subject { commit.diff_refs }
+
+ context 'when shas are missing' do
+ let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: nil) }
+
+ it 'returns false' do
+ expect(subject).not_to eq(other)
+ end
+ end
+
+ context 'when shas are equal' do
+ let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha) }
+
+ it 'returns true' do
+ expect(subject).to eq(other)
+ end
+ end
+
+ context 'when shas are unequal' do
+ let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha.reverse) }
+
+ it 'returns false' do
+ expect(subject).not_to eq(other)
+ end
+ end
+
+ context 'when shas are truncated' do
+ context 'when sha prefixes are too short' do
+ let(:other) { described_class.new(base_sha: subject.base_sha[0, 4], start_sha: subject.start_sha[0, 4], head_sha: subject.head_sha[0, 4]) }
+
+ it 'returns false' do
+ expect(subject).not_to eq(other)
+ end
+ end
+
+ context 'when sha prefixes are equal' do
+ let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10]) }
+
+ it 'returns true' do
+ expect(subject).to eq(other)
+ end
+ end
+
+ context 'when sha prefixes are unequal' do
+ let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10].reverse) }
+
+ it 'returns false' do
+ expect(subject).not_to eq(other)
+ end
+ end
+ end
+ end
+
describe '#compare_in' do
context 'with diff refs for the initial commit' do
let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
new file mode 100644
index 00000000000..2f99febe04e
--- /dev/null
+++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Formatters::ImageFormatter do
+ it_behaves_like "position formatter" do
+ let(:base_attrs) do
+ {
+ base_sha: 123,
+ start_sha: 456,
+ head_sha: 789,
+ old_path: 'old_image.png',
+ new_path: 'new_image.png',
+ position_type: 'image'
+ }
+ end
+
+ let(:attrs) do
+ base_attrs.merge(width: 100, height: 100, x: 1, y: 2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
new file mode 100644
index 00000000000..897dc917f6a
--- /dev/null
+++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Formatters::TextFormatter do
+ let!(:base) do
+ {
+ base_sha: 123,
+ start_sha: 456,
+ head_sha: 789,
+ old_path: 'old_path.txt',
+ new_path: 'new_path.txt'
+ }
+ end
+
+ let!(:complete) do
+ base.merge(old_line: 1, new_line: 2)
+ end
+
+ it_behaves_like "position formatter" do
+ let(:base_attrs) { base }
+
+ let(:attrs) { complete }
+ end
+
+ # Specific text formatter examples
+ let!(:formatter) { described_class.new(attrs) }
+
+ describe '#line_age' do
+ subject { formatter.line_age }
+
+ context ' when there is only new_line' do
+ let(:attrs) { base.merge(new_line: 1) }
+
+ it { is_expected.to eq('new') }
+ end
+
+ context ' when there is only old_line' do
+ let(:attrs) { base.merge(old_line: 1) }
+
+ it { is_expected.to eq('old') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index d4a2a852c12..9bf54fdecc4 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do
let(:project) { create(:project, :repository) }
- describe "position for an added file" do
+ describe "position for an added text file" do
let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") }
subject do
@@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do
end
end
+ describe "position for an added image file" do
+ let(:commit) { project.commit("33f3729a45c02fc67d00adb1b8bca394b0e761d9") }
+
+ subject do
+ described_class.new(
+ old_path: "files/images/6049019_460s.jpg",
+ new_path: "files/images/6049019_460s.jpg",
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ diff_refs: commit.diff_refs,
+ position_type: "image"
+ )
+ end
+
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file?).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
describe "position for a changed file" do
let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
@@ -429,27 +454,93 @@ describe Gitlab::Diff::Position do
end
end
- describe "#to_json" do
- let(:hash) do
- {
+ describe '#==' do
+ let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
+
+ subject do
+ described_class.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: 14,
- base_sha: nil,
- head_sha: nil,
- start_sha: nil
- }
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ context 'when positions are equal' do
+ let(:other) { described_class.new(subject.to_h) }
+
+ it 'returns true' do
+ expect(subject).to eq(other)
+ end
end
- let(:diff_position) { described_class.new(hash) }
+ context 'when positions are equal, except for truncated shas' do
+ let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha[0, 10])) }
+
+ it 'returns true' do
+ expect(subject).to eq(other)
+ end
+ end
+
+ context 'when positions are unequal' do
+ let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha.reverse)) }
+
+ it 'returns false' do
+ expect(subject).not_to eq(other)
+ end
+ end
+ end
+
+ describe "#to_json" do
+ shared_examples "diff position json" do
+ it "returns the position as JSON" do
+ expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ end
+
+ it "works when nested under another hash" do
+ expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ end
+ end
+
+ context "for text positon" do
+ let(:hash) do
+ {
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil,
+ position_type: "text"
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
- it "returns the position as JSON" do
- expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ it_behaves_like "diff position json"
end
- it "works when nested under another hash" do
- expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ context "for image positon" do
+ let(:hash) do
+ {
+ old_path: "files/any.img",
+ new_path: "files/any.img",
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ position_type: "image"
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
+
+ it_behaves_like "diff position json"
end
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 8beebc10040..e5138705443 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do
Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id)
end
+ def text_position_attrs
+ [:old_line, :new_line]
+ end
+
def position(attrs = {})
attrs.reverse_merge!(
diff_refs: old_diff_refs
@@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do
expect(new_position.diff_refs).to eq(new_diff_refs)
attrs.each do |attr, value|
- expect(new_position.send(attr)).to eq(value)
+ if text_position_attrs.include?(attr)
+ expect(new_position.formatter.send(attr)).to eq(value)
+ else
+ expect(new_position.send(attr)).to eq(value)
+ end
end
end
end
@@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do
expect(change_position.diff_refs).to eq(change_diff_refs)
attrs.each do |attr, value|
- expect(change_position.send(attr)).to eq(value)
+ if text_position_attrs.include?(attr)
+ expect(change_position.formatter.send(attr)).to eq(value)
+ else
+ expect(change_position.send(attr)).to eq(value)
+ end
end
end
end
@@ -1761,17 +1773,9 @@ describe Gitlab::Diff::PositionTracer do
let(:merge_commit) do
update_file_again_commit
- committer = repository.user_to_committer(current_user)
-
- options = {
- message: "Merge branches",
- author: committer,
- committer: committer
- }
-
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index a3d323fe28a..7dc06c90078 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -1,11 +1,14 @@
require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
- let(:text) { 'some text' }
- let(:old_project) { create(:project, name: 'old-project') }
- let(:new_project) { create(:project, name: 'new-project') }
+ let(:group) { create(:group) }
+ let(:old_project) { create(:project, name: 'old-project', group: group) }
+ let(:new_project) { create(:project, name: 'new-project', group: group) }
let(:user) { create(:user) }
+ let(:old_project_ref) { old_project.to_reference(new_project) }
+ let(:text) { 'some text' }
+
before do
old_project.team << [user, :reporter]
end
@@ -39,7 +42,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.not_to include merge_request.to_reference(new_project) }
end
- context 'description ambigous elements' do
+ context 'rewrite ambigous references' do
context 'url' do
let(:url) { 'http://gitlab.com/#1' }
let(:text) { "This references #1, but not #{url}" }
@@ -66,23 +69,21 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
- let(:project_ref) { old_project.to_reference(new_project) }
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
end
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
- let(:project_ref) { old_project.to_reference(new_project) }
before do
old_project.update(namespace: old_group)
@@ -90,21 +91,53 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
end
end
+ end
+
+ context 'reference contains project milestone' do
+ let!(:milestone) do
+ create(:milestone, title: '9.0', project: old_project)
+ end
+
+ let(:text) { 'milestone: %"9.0"' }
+
+ it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] }
+ end
+
+ context 'when referring to group milestone' do
+ let!(:milestone) do
+ create(:milestone, title: '10.0', group: group)
+ end
+
+ let(:text) { 'milestone %"10.0"' }
+
+ it { is_expected.to eq text }
+ end
+
+ context 'when referable has a nil reference' do
+ before do
+ create(:milestone, title: '9.0', project: old_project)
+
+ allow_any_instance_of(Milestone)
+ .to receive(:to_reference)
+ .and_return(nil)
+ end
- context 'reference contains milestone' do
- let(:milestone) { create(:milestone) }
- let(:text) { "milestone ref: #{milestone.to_reference}" }
+ let(:text) { 'milestone: %"9.0"' }
- it { is_expected.to eq text }
+ it 'raises an error that should be fixed' do
+ expect { subject }.to raise_error(
+ described_class::RewriteError,
+ 'Unspecified reference detected for Milestone'
+ )
end
end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 465c2012b05..793228701cf 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
it_behaves_like 'blaming a file'
end
- context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do
it_behaves_like 'blaming a file'
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 66ba00acb7d..412a0093d97 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -112,17 +112,20 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs'
end
- context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs'
end
end
shared_examples 'finding blobs by ID' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
+ let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
+
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
it { expect(raw_blob.size).to eq(669) }
it { expect(raw_blob.truncated?).to be_falsey }
+ it { expect(bad_blob).to be_nil }
context 'large file' do
it 'limits the size of a large file' do
@@ -147,7 +150,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs by ID'
end
- context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs by ID'
end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 14d64d8c4da..9f4e3c49adc 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -65,34 +65,12 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe "Commit info from gitaly commit" do
- let(:id) { 'f00' }
- let(:parent_ids) { %w(b45 b46) }
let(:subject) { "My commit".force_encoding('ASCII-8BIT') }
let(:body) { subject + "My body".force_encoding('ASCII-8BIT') }
- let(:committer) do
- Gitaly::CommitAuthor.new(
- name: generate(:name),
- email: generate(:email),
- date: Google::Protobuf::Timestamp.new(seconds: 123)
- )
- end
- let(:author) do
- Gitaly::CommitAuthor.new(
- name: generate(:name),
- email: generate(:email),
- date: Google::Protobuf::Timestamp.new(seconds: 456)
- )
- end
- let(:gitaly_commit) do
- Gitaly::GitCommit.new(
- id: id,
- subject: subject,
- body: body,
- author: author,
- committer: committer,
- parent_ids: parent_ids
- )
- end
+ let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) }
+ let(:id) { gitaly_commit.id }
+ let(:committer) { gitaly_commit.committer }
+ let(:author) { gitaly_commit.author }
let(:commit) { described_class.new(repository, gitaly_commit) }
it { expect(commit.short_id).to eq(id[0..10]) }
@@ -104,7 +82,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(commit.author_name).to eq(author.name) }
it { expect(commit.committer_name).to eq(committer.name) }
it { expect(commit.committer_email).to eq(committer.email) }
- it { expect(commit.parent_ids).to eq(parent_ids) }
+ it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) }
context 'no body' do
let(:body) { "".force_encoding('ASCII-8BIT') }
@@ -181,7 +159,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '.where' do
+ shared_examples '.where' do
context 'path is empty string' do
subject do
commits = described_class.where(
@@ -279,6 +257,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
+ describe '.where with gitaly' do
+ it_should_behave_like '.where'
+ end
+
+ describe '.where without gitaly', :skip_gitaly_mock do
+ it_should_behave_like '.where'
+ end
+
describe '.between' do
subject do
commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
@@ -350,7 +336,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_behaves_like 'finding all commits'
end
- context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding all commits'
context 'while applying a sort order based on the `order` option' do
@@ -401,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '#stats' do
+ shared_examples '#stats' do
subject { commit.stats }
describe '#additions' do
@@ -415,6 +401,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
+ describe '#stats with gitaly on' do
+ it_should_behave_like '#stats'
+ end
+
+ describe '#stats with gitaly disabled', :skip_gitaly_mock do
+ it_should_behave_like '#stats'
+ end
+
describe '#to_diff' do
subject { commit.to_diff }
diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/committer_spec.rb
deleted file mode 100644
index b0ddbb51449..00000000000
--- a/spec/lib/gitlab/git/committer_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Git::Committer do
- let(:name) { 'Jane Doe' }
- let(:email) { 'janedoe@example.com' }
- let(:gl_id) { 'user-123' }
-
- subject { described_class.new(name, email, gl_id) }
-
- describe '#==' do
- def eq_other(name, email, gl_id)
- eq(described_class.new(name, email, gl_id))
- end
-
- it { expect(subject).to eq_other(name, email, gl_id) }
-
- it { expect(subject).not_to eq_other(nil, nil, nil) }
- it { expect(subject).not_to eq_other(name + 'x', email, gl_id) }
- it { expect(subject).not_to eq_other(name, email + 'x', gl_id) }
- it { expect(subject).not_to eq_other(name, email, gl_id + 'x') }
- end
-end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 3494f0cc98d..ee657101f4c 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -341,8 +341,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when diff is quite large will collapse by default' do
- let(:iterator) { [{ diff: 'a' * (Gitlab::Git::Diff.collapse_limit + 1) }] }
- let(:max_files) { 100 }
+ let(:iterator) { [{ diff: 'a' * 20480 }] }
context 'when no collapse is set' do
let(:expanded) { true }
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index d39b33a0c05..4a7b06003fc 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -31,36 +31,6 @@ EOT
[".gitmodules"]).patches.first
end
- describe 'size limit feature toggles' do
- context 'when the feature gitlab_git_diff_size_limit_increase is enabled' do
- before do
- stub_feature_flags(gitlab_git_diff_size_limit_increase: true)
- end
-
- it 'returns 200 KB for size_limit' do
- expect(described_class.size_limit).to eq(200.kilobytes)
- end
-
- it 'returns 100 KB for collapse_limit' do
- expect(described_class.collapse_limit).to eq(100.kilobytes)
- end
- end
-
- context 'when the feature gitlab_git_diff_size_limit_increase is disabled' do
- before do
- stub_feature_flags(gitlab_git_diff_size_limit_increase: false)
- end
-
- it 'returns 100 KB for size_limit' do
- expect(described_class.size_limit).to eq(100.kilobytes)
- end
-
- it 'returns 10 KB for collapse_limit' do
- expect(described_class.collapse_limit).to eq(10.kilobytes)
- end
- end
- end
-
describe '.new' do
context 'using a Hash' do
context 'with a small diff' do
@@ -77,7 +47,7 @@ EOT
context 'using a diff that is too large' do
it 'prunes the diff' do
- diff = described_class.new(diff: 'a' * (described_class.size_limit + 1))
+ diff = described_class.new(diff: 'a' * 204800)
expect(diff.diff).to be_empty
expect(diff).to be_too_large
@@ -115,8 +85,8 @@ EOT
# The patch total size is 200, with lines between 21 and 54.
# This is a quick-and-dirty way to test this. Ideally, a new patch is
# added to the test repo with a size that falls between the real limits.
- allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(150)
- allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(100)
+ stub_const("#{described_class}::SIZE_LIMIT", 150)
+ stub_const("#{described_class}::COLLAPSE_LIMIT", 100)
end
it 'prunes the diff as a large diff instead of as a collapsed diff' do
@@ -356,7 +326,7 @@ EOT
describe '#collapsed?' do
it 'returns true for a diff that is quite large' do
- diff = described_class.new({ diff: 'a' * (described_class.collapse_limit + 1) }, expanded: false)
+ diff = described_class.new({ diff: 'a' * 20480 }, expanded: false)
expect(diff).to be_collapsed
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index ea3e4680b1d..2fe1f5603ce 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -14,6 +14,7 @@ describe Gitlab::Git::Hook do
let(:repo_path) { repository.path }
let(:user) { create(:user) }
let(:gl_id) { Gitlab::GlId.gl_id(user) }
+ let(:gl_username) { user.username }
def create_hook(name)
FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
@@ -28,6 +29,7 @@ describe Gitlab::Git::Hook do
f.write(<<-HOOK)
echo 'regular message from the hook'
echo 'error message from the hook' 1>&2
+ echo 'error message from the hook line 2' 1>&2
exit 1
HOOK
end
@@ -41,6 +43,7 @@ describe Gitlab::Git::Hook do
let(:env) do
{
'GL_ID' => gl_id,
+ 'GL_USERNAME' => gl_username,
'PWD' => repo_path,
'GL_PROTOCOL' => 'web',
'GL_REPOSITORY' => gl_repository
@@ -58,7 +61,7 @@ describe Gitlab::Git::Hook do
.with(env, hook_path, chdir: repo_path).and_call_original
end
- status, errors = hook.trigger(gl_id, blank, blank, ref)
+ status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref)
expect(status).to be true
expect(errors).to be_blank
end
@@ -71,9 +74,9 @@ describe Gitlab::Git::Hook do
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
- status, errors = hook.trigger(gl_id, blank, blank, ref)
+ status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref)
expect(status).to be false
- expect(errors).to eq("error message from the hook\n")
+ expect(errors).to eq("error message from the hook<br>error message from the hook line 2<br>")
end
end
end
@@ -85,7 +88,7 @@ describe Gitlab::Git::Hook do
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
- status, errors = hook.trigger(gl_id, blank, blank, ref)
+ status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref)
expect(status).to be true
expect(errors).to be_nil
end
diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb
index e9c0209fe3b..51e4e3fdad1 100644
--- a/spec/lib/gitlab/git/hooks_service_spec.rb
+++ b/spec/lib/gitlab/git/hooks_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::HooksService, seed_helper: true do
- let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') }
+ let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') }
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
let(:service) { described_class.new }
@@ -18,7 +18,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- service.execute(committer, repository, @blankrev, @newrev, @ref) { }
+ service.execute(user, repository, @blankrev, @newrev, @ref) { }
end
end
@@ -28,7 +28,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(committer, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, @blankrev, @newrev, @ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
@@ -40,7 +40,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(committer, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, @blankrev, @newrev, @ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 556a148c3bc..1ee4acfd193 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -54,7 +54,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#rugged" do
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'raises a storage exception when storage is not available' do
broken_repo = described_class.new('broken', 'a/path.git', '')
@@ -384,11 +384,45 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do
it_behaves_like 'simple commit counting'
end
end
+ describe '#has_local_branches?' do
+ shared_examples 'check for local branches' do
+ it { expect(repository.has_local_branches?).to eq(true) }
+
+ context 'mutable' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+
+ after do
+ ensure_seeds
+ end
+
+ it 'returns false when there are no branches' do
+ # Sanity check
+ expect(repository.has_local_branches?).to eq(true)
+
+ FileUtils.rm_rf(File.join(repository.path, 'packed-refs'))
+ heads_dir = File.join(repository.path, 'refs/heads')
+ FileUtils.rm_rf(heads_dir)
+ FileUtils.mkdir_p(heads_dir)
+
+ expect(repository.has_local_branches?).to eq(false)
+ end
+ end
+ end
+
+ context 'with gitaly' do
+ it_behaves_like 'check for local branches'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like 'check for local branches'
+ end
+ end
+
describe "#delete_branch" do
shared_examples "deleting a branch" do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
@@ -419,7 +453,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like "deleting a branch"
end
- context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do
+ context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do
it_behaves_like "deleting a branch"
end
end
@@ -455,7 +489,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'creating a branch'
end
- context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'creating a branch'
end
end
@@ -481,7 +515,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'raises an error if it failed' do
- expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
+ expect(@repo).to receive(:popen).and_return(['Error', 1])
expect do
@repo.delete_refs('refs/heads/fix')
@@ -895,7 +929,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'extended commit counting'
end
- context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'extended commit counting'
end
end
@@ -962,7 +996,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'finding a branch'
end
- context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
it 'should reload Rugged::Repository and return master' do
@@ -1204,7 +1238,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of refs'
end
- context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of refs'
end
end
@@ -1226,7 +1260,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of tags'
end
- context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of tags'
end
end
@@ -1250,7 +1284,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of branches'
end
- context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of branches'
end
end
@@ -1327,11 +1361,134 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'languages'
- context 'with rugged', skip_gitaly_mock: true do
+ context 'with rugged', :skip_gitaly_mock do
it_behaves_like 'languages'
end
end
+ describe '#with_repo_branch_commit' do
+ context 'when comparing with the same repository' do
+ let(:start_repository) { repository }
+
+ context 'when the branch exists' do
+ let(:start_branch_name) { 'master' }
+
+ it 'yields the commit' do
+ expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
+ .to yield_with_args(an_instance_of(Gitlab::Git::Commit))
+ end
+ end
+
+ context 'when the branch does not exist' do
+ let(:start_branch_name) { 'definitely-not-master' }
+
+ it 'yields nil' do
+ expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
+ .to yield_with_args(nil)
+ end
+ end
+ end
+
+ context 'when comparing with another repository' do
+ let(:start_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+
+ context 'when the branch exists' do
+ let(:start_branch_name) { 'master' }
+
+ it 'yields the commit' do
+ expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
+ .to yield_with_args(an_instance_of(Gitlab::Git::Commit))
+ end
+ end
+
+ context 'when the branch does not exist' do
+ let(:start_branch_name) { 'definitely-not-master' }
+
+ it 'yields nil' do
+ expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
+ .to yield_with_args(nil)
+ end
+ end
+ end
+ end
+
+ describe '#fetch_source_branch' do
+ let(:local_ref) { 'refs/merge-requests/1/head' }
+
+ context 'when the branch exists' do
+ let(:source_branch) { 'master' }
+
+ it 'writes the ref' do
+ expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/)
+
+ repository.fetch_source_branch(repository, source_branch, local_ref)
+ end
+
+ it 'returns true' do
+ expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true)
+ end
+ end
+
+ context 'when the branch does not exist' do
+ let(:source_branch) { 'definitely-not-master' }
+
+ it 'does not write the ref' do
+ expect(repository).not_to receive(:write_ref)
+
+ repository.fetch_source_branch(repository, source_branch, local_ref)
+ end
+
+ it 'returns false' do
+ expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false)
+ end
+ end
+ end
+
+ describe '#rm_branch' do
+ shared_examples "user deleting a branch" do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw }
+ let(:user) { create(:user) }
+ let(:branch_name) { "to-be-deleted-soon" }
+
+ before do
+ project.team << [user, :developer]
+ repository.create_branch(branch_name)
+ end
+
+ it "removes the branch from the repo" do
+ repository.rm_branch(branch_name, user: user)
+
+ expect(repository.rugged.branches[branch_name]).to be_nil
+ end
+ end
+
+ context "when Gitaly user_delete_branch is enabled" do
+ it_behaves_like "user deleting a branch"
+ end
+
+ context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do
+ it_behaves_like "user deleting a branch"
+ end
+ end
+
+ describe '#write_ref' do
+ context 'validations' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ref_path, :ref) do
+ 'foo bar' | '123'
+ 'foobar' | "12\x003"
+ end
+
+ with_them do
+ it 'raises ArgumentError' do
+ expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError)
+ end
+ end
+ end
+ end
+
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index b051a088171..c0eac98d718 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(Gitlab::Popen).to receive(:popen).with([
+ expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
@@ -36,7 +36,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(Gitlab::Popen).to receive(:popen).with([
+ expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index c86353abb7c..98cf7966dad 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
let(:storage_name) { 'default' }
- let(:circuit_breaker) { described_class.new(storage_name) }
+ let(:circuit_breaker) { described_class.new(storage_name, hostname) }
let(:hostname) { Gitlab::Environment.hostname }
let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
@@ -22,7 +22,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
'failure_wait_time' => 30,
'failure_reset_time' => 1800,
'storage_timeout' => 5
- }
+ },
+ 'nopath' => { 'path' => nil }
)
end
@@ -59,6 +60,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(breaker).to be_a(described_class)
expect(described_class.for_storage('default')).to eq(breaker)
end
+
+ it 'returns a broken circuit breaker for an unknown storage' do
+ expect(described_class.for_storage('unknown').circuit_broken?).to be_truthy
+ end
+
+ it 'returns a broken circuit breaker when the path is not set' do
+ expect(described_class.for_storage('nopath').circuit_broken?).to be_truthy
+ end
end
describe '#initialize' do
diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
new file mode 100644
index 00000000000..0e645008c88
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::NullCircuitBreaker do
+ let(:storage) { 'default' }
+ let(:hostname) { 'localhost' }
+ let(:error) { nil }
+
+ subject(:breaker) { described_class.new(storage, hostname, error: error) }
+
+ context 'with an error' do
+ let(:error) { Gitlab::Git::Storage::Misconfiguration.new('error') }
+
+ describe '#perform' do
+ it { expect { breaker.perform { 'ok' } }.to raise_error(error) }
+ end
+
+ describe '#circuit_broken?' do
+ it { expect(breaker.circuit_broken?).to be_truthy }
+ end
+
+ describe '#last_failure' do
+ it { Timecop.freeze { expect(breaker.last_failure).to eq(Time.now) } }
+ end
+
+ describe '#failure_count' do
+ it { expect(breaker.failure_count).to eq(breaker.failure_count_threshold) }
+ end
+
+ describe '#failure_info' do
+ it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } }
+ end
+ end
+
+ context 'not broken' do
+ describe '#perform' do
+ it { expect(breaker.perform { 'ok' }).to eq('ok') }
+ end
+
+ describe '#circuit_broken?' do
+ it { expect(breaker.circuit_broken?).to be_falsy }
+ end
+
+ describe '#last_failure' do
+ it { expect(breaker.last_failure).to be_nil }
+ end
+
+ describe '#failure_count' do
+ it { expect(breaker.failure_count).to eq(0) }
+ end
+
+ describe '#failure_info' do
+ it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) }
+ end
+ end
+
+ describe '#failure_count_threshold' do
+ it { expect(breaker.failure_count_threshold).to eq(1) }
+ end
+
+ it 'implements the CircuitBreaker interface' do
+ ours = described_class.public_instance_methods
+ theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods
+
+ # These methods are not part of the public API, but are public to allow the
+ # CircuitBreaker specs to operate. They should be made private over time.
+ exceptions = %i[
+ cache_key
+ check_storage_accessible!
+ no_failures?
+ storage_available?
+ track_storage_accessible
+ track_storage_inaccessible
+ ]
+
+ expect(theirs - ours).to contain_exactly(*exceptions)
+ end
+end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index cc10679ef1e..6c4f538bf01 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Git::Tag, seed_helper: true do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
- context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
end
diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb
new file mode 100644
index 00000000000..31d5f59a562
--- /dev/null
+++ b/spec/lib/gitlab/git/user_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Git::User do
+ let(:username) { 'janedo' }
+ let(:name) { 'Jane Doe' }
+ let(:email) { 'janedoe@example.com' }
+ let(:gl_id) { 'user-123' }
+
+ subject { described_class.new(username, name, email, gl_id) }
+
+ describe '.from_gitaly' do
+ let(:gitaly_user) { Gitaly::User.new(name: name, email: email, gl_id: gl_id) }
+ subject { described_class.from_gitaly(gitaly_user) }
+
+ it { expect(subject).to eq(described_class.new('', name, email, gl_id)) }
+ end
+
+ describe '.from_gitlab' do
+ let(:user) { build(:user) }
+ subject { described_class.from_gitlab(user) }
+
+ it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) }
+ end
+
+ describe '#==' do
+ def eq_other(username, name, email, gl_id)
+ eq(described_class.new(username, name, email, gl_id))
+ end
+
+ it { expect(subject).to eq_other(username, name, email, gl_id) }
+
+ it { expect(subject).not_to eq_other(nil, nil, nil, nil) }
+ it { expect(subject).not_to eq_other(username + 'x', name, email, gl_id) }
+ it { expect(subject).not_to eq_other(username, name + 'x', email, gl_id) }
+ it { expect(subject).not_to eq_other(username, name, email + 'x', gl_id) }
+ it { expect(subject).not_to eq_other(username, name, email, gl_id + 'x') }
+ end
+end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 458627ee4de..c9643c5da47 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: 4096)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
@@ -177,7 +177,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
@@ -598,6 +598,19 @@ describe Gitlab::GitAccess do
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
end
end
+
+ context "when in a read-only GitLab instance" do
+ before do
+ create(:protected_branch, name: 'feature', project: project)
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ # Only check admin; if an admin can't do it, other roles can't either
+ matrix = permissions_matrix[:admin].dup
+ matrix.each { |key, _| matrix[key] = false }
+
+ run_permission_checks(admin: matrix)
+ end
end
describe 'build authentication abilities' do
@@ -632,6 +645,16 @@ describe Gitlab::GitAccess do
end
end
+ context 'when the repository is read only' do
+ let(:project) { create(:project, :repository, :read_only) }
+
+ it 'denies push access' do
+ project.add_master(user)
+
+ expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
+ end
+ end
+
describe 'deploy key permissions' do
let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 0376b4ee783..1056074264a 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do
let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
let(:redirected_path) { nil }
let(:authentication_abilities) do
[
@@ -13,19 +14,27 @@ describe Gitlab::GitAccessWiki do
]
end
- describe 'push_allowed?' do
- before do
- create(:protected_branch, name: 'master', project: project)
- project.team << [user, :developer]
- end
+ describe '#push_access_check' do
+ context 'when user can :create_wiki' do
+ before do
+ create(:protected_branch, name: 'master', project: project)
+ project.team << [user, :developer]
+ end
- subject { access.check('git-receive-pack', changes) }
+ subject { access.check('git-receive-pack', changes) }
- it { expect { subject }.not_to raise_error }
- end
+ it { expect { subject }.not_to raise_error }
+
+ context 'when in a read-only GitLab instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
- def changes
- ['6f6d7e7ed 570e7b2ab refs/heads/master']
+ it 'does not give access to upload wiki code' do
+ expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
+ end
+ end
+ end
end
describe '#access_check_download!' do
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index e1fa8ae03f8..ba7fb168a3b 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::GitRefValidator do
it { expect(described_class.validate('feature/new')).to be_truthy }
it { expect(described_class.validate('implement_@all')).to be_truthy }
it { expect(described_class.validate('my_new_feature')).to be_truthy }
+ it { expect(described_class.validate('my-branch')).to be_truthy }
it { expect(described_class.validate('#1')).to be_truthy }
it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
it { expect(described_class.validate('feature/~new/')).to be_falsey }
@@ -22,4 +23,8 @@ describe Gitlab::GitRefValidator do
it { expect(described_class.validate('refs/remotes/')).to be_falsey }
it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
+ it { expect(described_class.validate('-')).to be_falsey }
+ it { expect(described_class.validate('-branch')).to be_falsey }
+ it { expect(described_class.validate('.tag')).to be_falsey }
+ it { expect(described_class.validate('my branch')).to be_falsey }
end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 4702a978f19..494dfe0e595 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe Gitlab::Git do
@@ -29,4 +30,12 @@ describe Gitlab::Git do
end
end
end
+
+ describe '.ref_name' do
+ it 'ensure ref is a valid UTF-8 string' do
+ utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5"
+
+ expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_Ã¥")
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index f32fe5d8150..b2275119a04 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -51,6 +51,10 @@ describe Gitlab::GitalyClient::CommitService do
expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher)
end
+
+ it 'encodes paths correctly' do
+ expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt', nil]) }.not_to raise_error
+ end
end
describe '#commit_deltas' do
@@ -165,4 +169,29 @@ describe Gitlab::GitalyClient::CommitService do
expect(subject).to eq("my diff")
end
end
+
+ describe '#commit_stats' do
+ let(:request) do
+ Gitaly::CommitStatsRequest.new(
+ repository: repository_message, revision: revision
+ )
+ end
+ let(:response) do
+ Gitaly::CommitStatsResponse.new(
+ oid: revision,
+ additions: 11,
+ deletions: 15
+ )
+ end
+
+ subject { described_class.new(repository).commit_stats(revision) }
+
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats)
+ .with(request, kind_of(Hash)).and_return(response)
+
+ expect(subject.additions).to eq(11)
+ expect(subject.deletions).to eq(15)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
new file mode 100644
index 00000000000..7bd6a7fa842
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::OperationService do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository.raw }
+ let(:client) { described_class.new(repository) }
+ let(:user) { create(:user) }
+ let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
+
+ describe '#user_create_branch' do
+ let(:branch_name) { 'new' }
+ let(:start_point) { 'master' }
+ let(:request) do
+ Gitaly::UserCreateBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch_name: branch_name,
+ start_point: start_point,
+ user: gitaly_user
+ )
+ end
+ let(:gitaly_commit) { build(:gitaly_commit) }
+ let(:commit_id) { gitaly_commit.id }
+ let(:gitaly_branch) do
+ Gitaly::Branch.new(name: branch_name, target_commit: gitaly_commit)
+ end
+ let(:response) { Gitaly::UserCreateBranchResponse.new(branch: gitaly_branch) }
+ let(:commit) { Gitlab::Git::Commit.new(repository, gitaly_commit) }
+
+ subject { client.user_create_branch(branch_name, user, start_point) }
+
+ it 'sends a user_create_branch message and returns a Gitlab::git::Branch' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_create_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject.name).to eq(branch_name)
+ expect(subject.dereferenced_target).to eq(commit)
+ end
+
+ context "when pre_receive_error is present" do
+ let(:response) do
+ Gitaly::UserCreateBranchResponse.new(pre_receive_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_create_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::HooksService::PreReceiveError, "something failed")
+ end
+ end
+ end
+
+ describe '#user_delete_branch' do
+ let(:branch_name) { 'my-branch' }
+ let(:request) do
+ Gitaly::UserDeleteBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch_name: branch_name,
+ user: gitaly_user
+ )
+ end
+ let(:response) { Gitaly::UserDeleteBranchResponse.new }
+
+ subject { client.user_delete_branch(branch_name, user) }
+
+ it 'sends a user_delete_branch message' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_delete_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ subject
+ end
+
+ context "when pre_receive_error is present" do
+ let(:response) do
+ Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_delete_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::HooksService::PreReceiveError, "something failed")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 6f59750b4da..8127b4842b7 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -84,14 +84,14 @@ describe Gitlab::GitalyClient::RefService do
end
end
- describe '#find_ref_name', seed_helper: true do
+ describe '#find_ref_name', :seed_helper do
subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') }
it { is_expected.to be_utf8 }
it { is_expected.to eq('refs/heads/master') }
end
- describe '#ref_exists?', seed_helper: true do
+ describe '#ref_exists?', :seed_helper do
it 'finds the master branch ref' do
expect(client.ref_exists?('refs/heads/master')).to eq(true)
end
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
new file mode 100644
index 00000000000..498f6886bee
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Util do
+ describe '.repository' do
+ let(:repository_storage) { 'default' }
+ let(:relative_path) { 'my/repo.git' }
+ let(:gl_repository) { 'project-1' }
+ let(:git_object_directory) { '.git/objects' }
+ let(:git_alternate_object_directory) { '/dir/one:/dir/two' }
+
+ subject do
+ described_class.repository(repository_storage, relative_path, gl_repository)
+ end
+
+ it 'creates a Gitaly::Repository with the given data' do
+ expect(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY')
+ .and_return(git_object_directory)
+ expect(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES')
+ .and_return(git_alternate_object_directory)
+
+ expect(subject).to be_a(Gitaly::Repository)
+ expect(subject.storage_name).to eq(repository_storage)
+ expect(subject.relative_path).to eq(relative_path)
+ expect(subject.gl_repository).to eq(gl_repository)
+ expect(subject.git_object_directory).to eq(git_object_directory)
+ expect(subject.git_alternate_object_directories).to eq([git_alternate_object_directory])
+ end
+ end
+
+ describe '.gitaly_user' do
+ let(:user) { create(:user) }
+ let(:gl_id) { Gitlab::GlId.gl_id(user) }
+
+ subject { described_class.gitaly_user(user) }
+
+ it 'creates a Gitaly::User from a GitLab user' do
+ expect(subject).to be_a(Gitaly::User)
+ expect(subject.name).to eq(user.name)
+ expect(subject.email).to eq(user.email)
+ expect(subject.gl_id).to eq(gl_id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a9b861fcff2..a1f4e65b8d4 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -38,6 +38,144 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
end
end
+ describe 'encode' do
+ [
+ [nil, ""],
+ ["", ""],
+ [" ", " "],
+ %w(a1 a1),
+ ["ç¼–ç ", "\xE7\xBC\x96\xE7\xA0\x81".b]
+ ].each do |input, result|
+ it "encodes #{input.inspect} to #{result.inspect}" do
+ expect(described_class.encode(input)).to eq result
+ end
+ end
+ end
+
+ describe 'allow_n_plus_1_calls' do
+ context 'when RequestStore is enabled', :request_store do
+ it 'returns the result of the allow_n_plus_1_calls block' do
+ expect(described_class.allow_n_plus_1_calls { "result" }).to eq("result")
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ it 'returns the result of the allow_n_plus_1_calls block' do
+ expect(described_class.allow_n_plus_1_calls { "something" }).to eq("something")
+ end
+ end
+ end
+
+ describe 'enforce_gitaly_request_limits?' do
+ def call_gitaly(count = 1)
+ (1..count).each do
+ described_class.enforce_gitaly_request_limits(:test)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'allows up the maximum number of allowed calls' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
+ end
+
+ context 'when the maximum number of calls has been reached' do
+ before do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
+ end
+
+ it 'fails on the next call' do
+ expect { call_gitaly(1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+
+ it 'allows the maximum number of calls to be exceeded within an allow_n_plus_1_calls block' do
+ expect do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1)
+ end
+ end.not_to raise_error
+ end
+
+ context 'when the maximum number of calls has been reached within an allow_n_plus_1_calls block' do
+ before do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
+ end
+ end
+
+ it 'allows up to the maximum number of calls outside of an allow_n_plus_1_calls block' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
+ end
+
+ it 'does not allow the maximum number of calls to be exceeded outside of an allow_n_plus_1_calls block' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ it 'does not raise errors when the maximum number of allowed calls is exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error
+ end
+
+ it 'does not fail when the maximum number of calls is exceeded within an allow_n_plus_1_calls block' do
+ expect do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1)
+ end
+ end.not_to raise_error
+ end
+ end
+ end
+
+ describe 'get_request_count' do
+ context 'when RequestStore is enabled', :request_store do
+ context 'when enforce_gitaly_request_limits is called outside of allow_n_plus_1_calls blocks' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+
+ it 'counts gitaly calls' do
+ expect(described_class.get_request_count).to eq(1)
+ end
+ end
+
+ context 'when enforce_gitaly_request_limits is called inside and outside of allow_n_plus_1_calls blocks' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ described_class.allow_n_plus_1_calls do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+ end
+
+ it 'counts gitaly calls' do
+ expect(described_class.get_request_count).to eq(2)
+ end
+ end
+
+ context 'when reset_counts is called' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ described_class.reset_counts
+ end
+
+ it 'resets counts' do
+ expect(described_class.get_request_count).to eq(0)
+ end
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+
+ it 'returns zero' do
+ expect(described_class.get_request_count).to eq(0)
+ end
+ end
+ end
+
describe 'feature_enabled?' do
let(:feature_name) { 'my_feature' }
let(:real_feature_name) { "gitaly_#{feature_name}" }
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index b07462e4978..a6c99bc07d4 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -63,6 +63,45 @@ describe Gitlab::Gpg::Commit do
it_behaves_like 'returns the cached signature on second call'
end
+ context 'commit signed with a subkey' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User3.public_key, user: user
+ end
+
+ let(:gpg_key_subkey) do
+ gpg_key.subkeys.find_by(fingerprint: '0522DD29B98F167CD8421752E38FFCAF75ABD92A')
+ end
+
+ before do
+ allow(Rugged::Commit).to receive(:extract_signature)
+ .with(Rugged::Repository, commit_sha)
+ .and_return(
+ [
+ GpgHelpers::User3.signed_commit_signature,
+ GpgHelpers::User3.signed_commit_base_data
+ ]
+ )
+ end
+
+ it 'returns a valid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key_subkey,
+ gpg_key_primary_keyid: gpg_key_subkey.keyid,
+ gpg_key_user_name: GpgHelpers::User3.names.first,
+ gpg_key_user_email: GpgHelpers::User3.emails.first,
+ verification_status: 'verified'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
+ end
+
context 'user email does not match the committer email, but is the same user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index b9fd4d02156..d6000af0ecd 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -2,17 +2,16 @@ require 'rails_helper'
RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' do
- let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
- let!(:project) { create :project, :repository, path: 'sample-project' }
+ let(:signature) { [GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data] }
+ let(:committer_email) { GpgHelpers::User1.emails.first }
+ let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:raw_commit) do
raw_commit = double(
:raw_commit,
- signature: [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ],
+ signature: signature,
sha: commit_sha,
- committer_email: GpgHelpers::User1.emails.first
+ committer_email: committer_email
)
allow(raw_commit).to receive :save!
@@ -29,12 +28,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
+ .and_return(signature)
end
context 'gpg signature did have an associated gpg key which was removed later' do
@@ -183,5 +177,34 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
)
end
end
+
+ context 'gpg signature did not have an associated gpg subkey' do
+ let(:signature) { [GpgHelpers::User3.signed_commit_signature, GpgHelpers::User3.signed_commit_base_data] }
+ let(:committer_email) { GpgHelpers::User3.emails.first }
+ let!(:user) { create :user, email: GpgHelpers::User3.emails.first }
+
+ let!(:invalid_gpg_signature) do
+ create :gpg_signature,
+ project: project,
+ commit_sha: commit_sha,
+ gpg_key: nil,
+ gpg_key_primary_keyid: GpgHelpers::User3.subkey_fingerprints.last[24..-1],
+ verification_status: 'unknown_key'
+ end
+
+ it 'updates the signature to being valid when the missing gpg key is added' do
+ # InvalidGpgSignatureUpdater is called by the after_create hook
+ gpg_key = create(:gpg_key, key: GpgHelpers::User3.public_key, user: user)
+ subkey = gpg_key.subkeys.last
+
+ expect(invalid_gpg_signature.reload).to have_attributes(
+ project: project,
+ commit_sha: commit_sha,
+ gpg_key_subkey_id: subkey.id,
+ gpg_key_primary_keyid: subkey.keyid,
+ verification_status: 'verified'
+ )
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 11a2aea1915..ab9a166db00 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -28,6 +28,23 @@ describe Gitlab::Gpg do
end
end
+ describe '.subkeys_from_key' do
+ it 'returns the subkeys by primary key' do
+ all_subkeys = described_class.subkeys_from_key(GpgHelpers::User1.public_key)
+ subkeys = all_subkeys[GpgHelpers::User1.primary_keyid]
+
+ expect(subkeys).to be_present
+ expect(subkeys.first[:keyid]).to be_present
+ expect(subkeys.first[:fingerprint]).to be_present
+ end
+
+ it 'returns an empty array when there are not subkeys' do
+ all_subkeys = described_class.subkeys_from_key(GpgHelpers::User4.public_key)
+
+ expect(all_subkeys[GpgHelpers::User4.primary_keyid]).to be_empty
+ end
+ end
+
describe '.user_infos_from_key' do
it 'returns the names and emails' do
user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key)
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 08010c2d0e2..8dc83a6db7f 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#base_and_descendants' do
@@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#all_groups' do
@@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index f5c9680bf59..4c1ca4349ea 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
let(:metric_class) { Gitlab::HealthChecks::Metric }
let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { [:default] }
+ let(:repository_storages) { ['default'] }
let(:tmp_dir) { Dir.mktmpdir }
let(:storages_paths) do
@@ -44,7 +44,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
- context 'storage has a tripped circuitbreaker', broken_storage: true do
+ context 'storage has a tripped circuitbreaker', :broken_storage do
let(:repository_storages) { ['broken'] }
let(:storages_paths) do
Gitlab.config.repositories.storages
@@ -64,7 +64,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_circuitbreaker_test) { true }
end
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
end
context 'storage points to directory that has both read and write rights' do
@@ -72,7 +72,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
FileUtils.chmod_R(0755, tmp_dir)
end
- it { is_expected.to include(result_class.new(true, nil, shard: :default)) }
+ it { is_expected.to include(result_class.new(true, nil, shard: 'default')) }
it 'cleans up files used for testing' do
expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original
@@ -85,7 +85,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false)
end
- it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) }
end
context 'write test fails' do
@@ -93,7 +93,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false)
end
- it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) }
end
end
end
@@ -109,7 +109,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
@@ -128,7 +128,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
@@ -156,14 +156,14 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
end
describe '#metrics' do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3fb8edeb701..29baa70d5ae 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -147,6 +147,10 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
+cluster:
+- project
+- user
+- service
services:
- project
- service_hook
@@ -177,6 +181,7 @@ project:
- tag_taggings
- tags
- chat_services
+- cluster
- creator
- group
- namespace
@@ -266,6 +271,10 @@ project:
- container_repositories
- uploads
- members_and_requesters
+- build_trace_section_names
+- root_of_fork_network
+- fork_network_member
+- fork_network
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index c7fbc2bc92f..dd0ce0dae41 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -1,13 +1,15 @@
require 'spec_helper'
describe 'forked project import' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
let(:forked_from_project) { create(:project, :repository) }
- let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) }
+ let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
@@ -16,7 +18,7 @@ describe 'forked project import' do
end
let!(:merge_request) do
- create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo)
+ create(:merge_request, source_project: forked_project, target_project: project_with_repo)
end
let(:saver) do
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 4d87f27ce05..473ba40fae7 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
describe Gitlab::ImportExport::MergeRequestParser do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let(:forked_from_project) { create(:project, :repository) }
- let(:fork_link) { create(:forked_project_link, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let!(:merge_request) do
- create(:merge_request, source_project: fork_link.forked_to_project, target_project: project)
+ create(:merge_request, source_project: forked_project, target_project: project)
end
let(:parsed_merge_request) do
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 8e3554375e8..d9b86e1bf34 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -119,7 +119,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
it 'has no when YML attributes but only the DB column' do
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
- expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes)
+ expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
saved_project_json
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 96c70d1b675..6d4ee470b00 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -25,6 +25,7 @@ Issue:
- relative_position
- last_edited_at
- last_edited_by_id
+- discussion_locked
Event:
- id
- target_type
@@ -168,6 +169,7 @@ MergeRequest:
- last_edited_at
- last_edited_by_id
- head_pipeline_id
+- discussion_locked
MergeRequestDiff:
- id
- state
@@ -224,6 +226,7 @@ Ci::Pipeline:
- auto_canceled_by_id
- pipeline_schedule_id
- config_source
+- failure_reason
- protected
Ci::Stage:
- id
@@ -312,6 +315,32 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
+Gcp::Cluster:
+- id
+- project_id
+- user_id
+- service_id
+- enabled
+- status
+- status_reason
+- project_namespace
+- endpoint
+- ca_cert
+- encrypted_kubernetes_token
+- encrypted_kubernetes_token_iv
+- username
+- encrypted_password
+- encrypted_password_iv
+- gcp_project_id
+- gcp_cluster_zone
+- gcp_cluster_name
+- gcp_cluster_size
+- gcp_machine_type
+- gcp_operation_id
+- encrypted_gcp_token
+- encrypted_gcp_token_iv
+- created_at
+- updated_at
DeployKey:
- id
- user_id
@@ -414,6 +443,8 @@ Project:
- last_repository_updated_at
- ci_config_path
- delete_error
+- merge_requests_ff_only_enabled
+- merge_requests_rebase_enabled
Author:
- name
ProjectFeature:
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 8370adf9211..1785094af10 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::LDAP::AuthHash do
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
- uid: '123456',
+ uid: given_uid,
provider: 'ldapmain',
info: info,
extra: {
@@ -32,6 +32,8 @@ describe Gitlab::LDAP::AuthHash do
end
context "without overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
it "has the correct username" do
expect(auth_hash.username).to eq("123456")
end
@@ -42,6 +44,8 @@ describe Gitlab::LDAP::AuthHash do
end
context "with overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
let(:attributes) do
{
'username' => %w(mail email),
@@ -61,4 +65,22 @@ describe Gitlab::LDAP::AuthHash do
expect(auth_hash.name).to eq("John Smith")
end
end
+
+ describe '#uid' do
+ context 'when there is extraneous (but valid) whitespace' do
+ let(:given_uid) { 'uid =john smith , ou = people, dc= example,dc =com' }
+
+ it 'removes the extraneous whitespace' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+
+ context 'when there are upper case characters' do
+ let(:given_uid) { 'UID=John Smith,ou=People,dc=example,dc=com' }
+
+ it 'downcases' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb
new file mode 100644
index 00000000000..8e21ecdf9ab
--- /dev/null
+++ b/spec/lib/gitlab/ldap/dn_spec.rb
@@ -0,0 +1,224 @@
+require 'spec_helper'
+
+describe Gitlab::LDAP::DN do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#normalize_value' do
+ subject { described_class.normalize_value(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { '"Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { '"James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'foo\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+ end
+ end
+
+ describe '#to_normalized_s' do
+ subject { described_class.new(given).to_normalized_s }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'when we do not support the given DN format' do
+ context 'multivalued RDNs' do
+ context 'without extraneous whitespace' do
+ let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'with extraneous whitespace' do
+ context 'around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'not around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'uid=John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { 'uid="Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'without a name value pair' do
+ let(:given) { 'John' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { 'cn="James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'cn=J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'cn=\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid OID attribute type name' do
+ let(:given) { '1.2.d=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"')
+ end
+ end
+
+ context 'with a period in a non-OID attribute type name' do
+ let(:given) { 'd1.2=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."')
+ end
+ end
+
+ context 'when starting with non-space, non-alphanumeric character' do
+ let(:given) { ' -uid=John Smith' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"')
+ end
+ end
+
+ context 'when given a UID with an escaped equal sign' do
+ let(:given) { 'uid\\=john' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"')
+ end
+ end
+ end
+ end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: \"#{expected}\"\n got: \"#{got}\""
+ expect(got).to eq(expected), test_failure_message
+ end
+end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index 087c4d8c92c..d204050ef66 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -16,6 +16,34 @@ describe Gitlab::LDAP::Person do
)
end
+ describe '.normalize_dn' do
+ subject { described_class.normalize_dn(given) }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given DN unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
+ describe '.normalize_uid' do
+ subject { described_class.normalize_uid(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given UID unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
describe '#name' do
it 'uses the configured name attribute and handles values as an array' do
name = 'John Doe'
@@ -43,4 +71,9 @@ describe Gitlab::LDAP::Person do
expect(person.email).to eq([user_principal_name])
end
end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}"
+ expect(got).to eq(expected), test_failure_message
+ end
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 6a6e465cea2..9a4705d1cee 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash) do
- OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info)
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info)
end
let(:ldap_user_upper_case) { described_class.new(auth_hash_upper_case) }
let(:info_upper_case) do
@@ -22,12 +22,12 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash_upper_case) do
- OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case)
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
describe '#changed?' do
it "marks existing ldap user as changed" do
- create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
expect(ldap_user.changed?).to be_truthy
end
@@ -37,7 +37,7 @@ describe Gitlab::LDAP::User do
end
it "does not mark existing ldap user as changed" do
- create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true)
expect(ldap_user.changed?).to be_falsey
end
@@ -60,7 +60,7 @@ describe Gitlab::LDAP::User do
describe 'find or create' do
it "finds the user if already existing" do
- create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
expect { ldap_user.save }.not_to change { User.count }
end
@@ -70,7 +70,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
end
@@ -79,7 +79,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
expect(existing_user.id).to eql ldap_user.gl_user.id
end
@@ -89,7 +89,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user_upper_case.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
expect(existing_user.id).to eql ldap_user.gl_user.id
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 6af1564da19..cab662819ac 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -79,12 +79,28 @@ describe Gitlab::Middleware::Go do
it_behaves_like 'a nested project'
end
+ context 'with a subpackage that is not a valid project path' do
+ let(:path) { "#{project.full_path}/---subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
context 'without subpackages' do
let(:path) { project.full_path }
it_behaves_like 'a nested project'
end
end
+
+ context 'with a bogus path' do
+ let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+
+ it 'skips go-import generation' do
+ expect(app).to receive(:call).and_return('no-go')
+
+ go
+ end
+ end
end
def go
@@ -100,7 +116,7 @@ describe Gitlab::Middleware::Go do
def expect_response_with_path(response, path)
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n"
+ expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>}
expect(response[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
new file mode 100644
index 00000000000..742a792a1af
--- /dev/null
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::ReadOnly do
+ include Rack::Test::Methods
+
+ RSpec::Matchers.define :be_a_redirect do
+ match do |response|
+ response.status == 301
+ end
+ end
+
+ RSpec::Matchers.define :disallow_request do
+ match do |middleware|
+ flash = middleware.send(:rack_flash)
+ flash['alert'] && flash['alert'].include?('You cannot do writing operations')
+ end
+ end
+
+ RSpec::Matchers.define :disallow_request_in_json do
+ match do |response|
+ json_response = JSON.parse(response.body)
+ response.body.include?('You cannot do writing operations') && json_response.key?('message')
+ end
+ end
+
+ let(:rack_stack) do
+ rack = Rack::Builder.new do
+ use ActionDispatch::Session::CacheStore
+ use ActionDispatch::Flash
+ use ActionDispatch::ParamsParser
+ end
+
+ rack.run(subject)
+ rack.to_app
+ end
+
+ subject { described_class.new(fake_app) }
+
+ let(:request) { Rack::MockRequest.new(rack_stack) }
+
+ context 'normal requests to a read-only Gitlab instance' do
+ let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'expects PATCH requests to be disallowed' do
+ response = request.patch('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects PUT requests to be disallowed' do
+ response = request.put('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects POST requests to be disallowed' do
+ response = request.post('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects a internal POST request to be allowed after a disallowed request' do
+ response = request.post('/test_request')
+
+ expect(response).to be_a_redirect
+
+ response = request.post("/api/#{API::API.version}/internal")
+
+ expect(response).not_to be_a_redirect
+ end
+
+ it 'expects DELETE requests to be disallowed' do
+ response = request.delete('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ context 'whitelisted requests' do
+ it 'expects DELETE request to logout to be allowed' do
+ response = request.delete('/users/sign_out')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects a POST internal request to be allowed' do
+ response = request.post("/api/#{API::API.version}/internal")
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects a POST LFS request to batch URL to be allowed' do
+ response = request.post('/root/rouge.git/info/lfs/objects/batch')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+ end
+ end
+
+ context 'json requests to a read-only GitLab instance' do
+ let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } }
+ let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } }
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'expects PATCH requests to be disallowed' do
+ response = request.patch('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects PUT requests to be disallowed' do
+ response = request.put('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects POST requests to be disallowed' do
+ response = request.post('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects DELETE requests to be disallowed' do
+ response = request.delete('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+ end
+end
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index d5f4da3ce36..dbcc200b90b 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
describe Gitlab::OAuth::AuthHash do
+ let(:provider) { 'ldap'.freeze }
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
- provider: provider_ascii,
+ provider: provider,
uid: uid_ascii,
info: info_hash
)
@@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do
let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" }
- let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) }
let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) }
@@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do
let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) }
- let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) }
let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) }
let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) }
let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) }
@@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do
end
context 'defaults' do
- it { expect(auth_hash.provider).to eql provider_utf8 }
+ it { expect(auth_hash.provider).to eq provider }
it { expect(auth_hash.uid).to eql uid_utf8 }
it { expect(auth_hash.email).to eql email_utf8 }
it { expect(auth_hash.username).to eql nickname_utf8 }
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 8aaf320cbf5..db26e16e3b2 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::OAuth::User do
let(:oauth_user) { described_class.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
+ let(:dn) { 'uid=user1,ou=People,dc=example' }
let(:provider) { 'my-provider' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
@@ -197,7 +198,7 @@ describe Gitlab::OAuth::User do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
- allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
+ allow(ldap_user).to receive(:dn) { dn }
end
context "and no account for the LDAP user" do
@@ -213,7 +214,7 @@ describe Gitlab::OAuth::User do
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
[
- { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
@@ -221,7 +222,7 @@ describe Gitlab::OAuth::User do
end
context "and LDAP user has an account already" do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
@@ -234,7 +235,7 @@ describe Gitlab::OAuth::User do
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
[
- { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
@@ -252,7 +253,7 @@ describe Gitlab::OAuth::User do
expect(identities_as_hash)
.to match_array(
[
- { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
@@ -310,8 +311,8 @@ describe Gitlab::OAuth::User do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
- allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(oauth_user).to receive(:ldap_person).and_return(ldap_user)
+ allow(ldap_user).to receive(:dn) { dn }
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context "and no account for the LDAP user" do
@@ -341,7 +342,7 @@ describe Gitlab::OAuth::User do
end
context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
context 'dont block on create (LDAP)' do
before do
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 2f989397f7e..1f1c48ee9b5 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -84,9 +84,9 @@ describe Gitlab::PathRegex do
let(:top_level_words) do
words = routes_not_starting_in_wildcard.map do |route|
route.split('/')[1]
- end.compact.uniq
+ end.compact
- words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)
+ (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq
end
let(:ee_top_level_words) do
@@ -95,10 +95,11 @@ describe Gitlab::PathRegex do
let(:files_in_public) do
git = Gitlab.config.git.bin_path
- `cd #{Rails.root} && #{git} ls-files public`
+ tracked = `cd #{Rails.root} && #{git} ls-files public`
.split("\n")
.map { |entry| entry.gsub('public/', '') }
.uniq
+ tracked + %w(assets uploads)
end
# All routes that start with a namespaced path, that have 1 or more
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 4567f220c11..b145ca36f26 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -14,7 +14,7 @@ describe 'Gitlab::Popen' do
end
it { expect(@status).to be_zero }
- it { expect(@output).to include('cache') }
+ it { expect(@output).to include('tests') }
end
context 'non-zero status' do
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 19710029224..59923bfb14d 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -1,9 +1,12 @@
require 'spec_helper'
describe Gitlab::Saml::User do
+ include LdapHelpers
+
let(:saml_user) { described_class.new(auth_hash) }
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
+ let(:dn) { 'uid=user1,ou=People,dc=example' }
let(:provider) { 'saml' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) }
let(:info_hash) do
@@ -163,13 +166,17 @@ describe Gitlab::Saml::User do
end
context 'and a corresponding LDAP person' do
+ let(:adapter) { ldap_adapter('ldapmain') }
+
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
- allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+ allow(ldap_user).to receive(:dn) { dn }
+ allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
@@ -181,20 +188,86 @@ describe Gitlab::Saml::User do
expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
{ provider: 'saml', extern_uid: uid }])
end
end
context 'and LDAP user has an account already' do
+ let(:auth_hash_base_attributes) do
+ {
+ uid: uid,
+ provider: provider,
+ info: info_hash,
+ extra: {
+ raw_info: OneLogin::RubySaml::Attributes.new(
+ { 'groups' => %w(Developers Freelancers Designers) }
+ )
+ }
+ }
+ end
+ let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) }
+ let(:uid_types) { %w(uid dn email) }
+
before do
create(:omniauth_user,
email: 'john@mail.com',
- extern_uid: 'uid=user1,ou=People,dc=example',
+ extern_uid: dn,
provider: 'ldapmain',
username: 'john')
end
+ shared_examples 'find LDAP person' do |uid_type, uid|
+ let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes.merge(uid: extern_uid)) }
+
+ before do
+ nil_types = uid_types - [uid_type]
+
+ nil_types.each do |type|
+ allow(Gitlab::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil)
+ end
+
+ allow(Gitlab::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user)
+ end
+
+ it 'adds the omniauth identity to the LDAP account' do
+ identities = [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: extern_uid }
+ ]
+
+ identities_as_hash = gl_user.identities.map do |id|
+ { provider: id.provider, extern_uid: id.extern_uid }
+ end
+
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.email).to eql 'john@mail.com'
+ expect(gl_user.identities.length).to be 2
+ expect(identities_as_hash).to match_array(identities)
+ end
+ end
+
+ context 'when uid is an uid' do
+ it_behaves_like 'find LDAP person', 'uid' do
+ let(:extern_uid) { uid }
+ end
+ end
+
+ context 'when uid is a dn' do
+ it_behaves_like 'find LDAP person', 'dn' do
+ let(:extern_uid) { dn }
+ end
+ end
+
+ context 'when uid is an email' do
+ it_behaves_like 'find LDAP person', 'email' do
+ let(:extern_uid) { 'john@mail.com' }
+ end
+ end
+
it 'adds the omniauth identity to the LDAP account' do
saml_user.save
@@ -203,7 +276,7 @@ describe Gitlab::Saml::User do
expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
{ provider: 'saml', extern_uid: uid }])
end
@@ -219,17 +292,21 @@ describe Gitlab::Saml::User do
context 'user has SAML user, and wants to add their LDAP identity' do
it 'adds the LDAP identity to the existing SAML user' do
- create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john')
- local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash)
+ create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john')
+
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user)
+
+ local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash)
local_saml_user = described_class.new(local_hash)
+
local_saml_user.save
local_gl_user = local_saml_user.gl_user
expect(local_gl_user).to be_valid
expect(local_gl_user.identities.length).to be 2
identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: dn }])
end
end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 4c5efbde69a..e44a7c23452 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::SearchResults do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
@@ -42,7 +44,7 @@ describe Gitlab::SearchResults do
end
it 'includes merge requests from source and target projects' do
- forked_project = create(:project, forked_from_project: project)
+ forked_project = fork_project(project, user)
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
results = described_class.new(user, Project.where(id: forked_project.id), 'foo')
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index c7930378240..2158b2837e2 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -15,10 +15,6 @@ describe Gitlab::Shell do
it { is_expected.to respond_to :add_repository }
it { is_expected.to respond_to :remove_repository }
it { is_expected.to respond_to :fork_repository }
- it { is_expected.to respond_to :add_namespace }
- it { is_expected.to respond_to :rm_namespace }
- it { is_expected.to respond_to :mv_namespace }
- it { is_expected.to respond_to :exists? }
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
@@ -48,14 +44,35 @@ describe Gitlab::Shell do
end
end
- describe '#add_key' do
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
- )
+ describe 'projects commands' do
+ let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') }
+ let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') }
+ let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') }
+
+ before do
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
+ allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path)
+ allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
+ end
+
+ describe '#mv_repository' do
+ it 'executes the command' do
+ expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
+ [projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git']
+ )
+ gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path')
+ end
+ end
+
+ describe '#add_key' do
+ it 'removes trailing garbage' do
+ allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
+ expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
+ [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
+ )
- gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
end
end
@@ -105,30 +122,42 @@ describe Gitlab::Shell do
end
describe '#add_repository' do
- it 'creates a repository' do
- created_path = File.join(TestEnv.repos_path, 'project', 'path.git')
- hooks_path = File.join(created_path, 'hooks')
-
- begin
- result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path')
+ shared_examples '#add_repository' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repo_name) { 'project/path' }
+ let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
- repo_stat = File.stat(created_path) rescue nil
- hooks_stat = File.lstat(hooks_path) rescue nil
- hooks_dir = File.realpath(hooks_path)
- ensure
+ after do
FileUtils.rm_rf(created_path)
end
- expect(result).to be_truthy
- expect(repo_stat.mode & 0o777).to eq(0o770)
- expect(hooks_stat.symlink?).to be_truthy
- expect(hooks_dir).to eq(gitlab_shell_hooks_path)
+ it 'creates a repository' do
+ expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_truthy
+
+ expect(File.stat(created_path).mode & 0o777).to eq(0o770)
+
+ hooks_path = File.join(created_path, 'hooks')
+ expect(File.lstat(hooks_path)).to be_symlink
+ expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
+ end
+
+ it 'returns false when the command fails' do
+ FileUtils.mkdir_p(File.dirname(created_path))
+ # This file will block the creation of the repo's .git directory. That
+ # should cause #add_repository to fail.
+ FileUtils.touch(created_path)
+
+ expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_falsy
+ end
end
- it 'returns false when the command fails' do
- expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST)
+ context 'with gitaly' do
+ it_behaves_like '#add_repository'
+ end
- expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#add_repository'
end
end
@@ -136,7 +165,7 @@ describe Gitlab::Shell do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
.with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return([nil, 0])
+ nil, popen_vars).and_return([nil, 0])
expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true
end
@@ -144,7 +173,7 @@ describe Gitlab::Shell do
it 'returns false when the command fails' do
expect(Gitlab::Popen).to receive(:popen)
.with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return(["error", 1])
+ nil, popen_vars).and_return(["error", 1])
expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false
end
@@ -304,7 +333,7 @@ describe Gitlab::Shell do
end
end
- describe '#fetch_remote local', skip_gitaly_mock: true do
+ describe '#fetch_remote local', :skip_gitaly_mock do
it_should_behave_like 'fetch_remote', false
end
@@ -330,4 +359,52 @@ describe Gitlab::Shell do
end
end
end
+
+ describe 'namespace actions' do
+ subject { described_class.new }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.path }
+
+ describe '#add_namespace' do
+ it 'creates a namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ end
+ end
+
+ describe '#exists?' do
+ context 'when the namespace does not exist' do
+ it 'returns false' do
+ expect(subject.exists?(storage_path, "non-existing")).to be(false)
+ end
+ end
+
+ context 'when the namespace exists' do
+ it 'returns true' do
+ subject.add_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ end
+ end
+ end
+
+ describe '#remove' do
+ it 'removes the namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+ subject.rm_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ end
+ end
+
+ describe '#mv_namespace' do
+ it 'renames the namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+ subject.mv_namespace(storage_path, "mepmep", "2mep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ expect(subject.exists?(storage_path, "2mep")).to be(true)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index baf8f6644bf..8026fba9f0a 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do
expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
end
+
+ it 'uses UNION ALL when removing duplicates is disabled' do
+ union = described_class
+ .new([relation_1, relation_2], remove_duplicates: false)
+
+ expect(union.to_sql).to include('UNION ALL')
+ end
end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index fdc3990132a..fc8991fd31f 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -39,7 +39,8 @@ describe Gitlab::UrlSanitizer do
false | nil
false | ''
false | '123://invalid:url'
- true | 'valid@project:url.git'
+ false | 'valid@project:url.git'
+ false | 'valid:pass@project:url.git'
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
@@ -81,24 +82,6 @@ describe Gitlab::UrlSanitizer do
describe '#credentials' do
context 'credentials in hash' do
- where(:input, :output) do
- { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' }
- { user: 'foo', password: '' } | { user: 'foo', password: nil }
- { user: 'foo', password: nil } | { user: 'foo', password: nil }
- { user: '', password: 'bar' } | { user: nil, password: 'bar' }
- { user: '', password: '' } | { user: nil, password: nil }
- { user: '', password: nil } | { user: nil, password: nil }
- { user: nil, password: 'bar' } | { user: nil, password: 'bar' }
- { user: nil, password: '' } | { user: nil, password: nil }
- { user: nil, password: nil } | { user: nil, password: nil }
- end
-
- with_them do
- subject { described_class.new('user@example.com:path.git', credentials: input).credentials }
-
- it { is_expected.to eq(output) }
- end
-
it 'overrides URL-provided credentials' do
sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
@@ -116,10 +99,6 @@ describe Gitlab::UrlSanitizer do
'http://@example.com' | { user: nil, password: nil }
'http://example.com' | { user: nil, password: nil }
- # Credentials from SCP-style URLs are not supported at present
- 'foo@example.com:path' | { user: nil, password: nil }
- 'foo:bar@example.com:path' | { user: nil, password: nil }
-
# Other invalid URLs
nil | { user: nil, password: nil }
'' | { user: nil, password: nil }
@@ -174,4 +153,13 @@ describe Gitlab::UrlSanitizer do
end
end
end
+
+ context 'when credentials contains special chars' do
+ it 'should parse the URL without errors' do
+ url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git")
+
+ expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git")
+ expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git")
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 68429d792f2..777e9c8e21d 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -26,6 +26,16 @@ describe Gitlab::UsageData do
version
uuid
hostname
+ signup
+ ldap
+ gravatar
+ omniauth
+ reply_by_email
+ container_registry
+ gitlab_pages
+ gitlab_shared_runners
+ git
+ database
))
end
@@ -40,12 +50,17 @@ describe Gitlab::UsageData do
ci_builds
ci_internal_pipelines
ci_external_pipelines
+ ci_pipeline_config_auto_devops
+ ci_pipeline_config_repository
ci_runners
ci_triggers
ci_pipeline_schedules
+ auto_devops_enabled
+ auto_devops_disabled
deploy_keys
deployments
environments
+ gcp_clusters
in_review_folder
groups
issues
@@ -82,6 +97,32 @@ describe Gitlab::UsageData do
end
end
+ describe '#features_usage_data_ce' do
+ subject { described_class.features_usage_data_ce }
+
+ it 'gathers feature usage data' do
+ expect(subject[:signup]).to eq(current_application_settings.signup_enabled?)
+ expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
+ expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?)
+ expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
+ expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?)
+ expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled)
+ expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
+ end
+ end
+
+ describe '#components_usage_data' do
+ subject { described_class.components_usage_data }
+
+ it 'gathers components usage data' do
+ expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
+ expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
+ expect(subject[:git][:version]).to eq(Gitlab::Git.version)
+ expect(subject[:database][:adapter]).to eq(Gitlab::Database.adapter_name)
+ expect(subject[:database][:version]).to eq(Gitlab::Database.version)
+ end
+ end
+
describe '#license_usage_data' do
subject { described_class.license_usage_data }
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 699184ad9fe..9230d58012f 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -13,13 +13,51 @@ describe Gitlab::Workhorse do
end
describe ".send_git_archive" do
+ let(:ref) { 'master' }
+ let(:format) { 'zip' }
+ let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
+ let(:base_params) { repository.archive_metadata(ref, storage_path, format) }
+ let(:gitaly_params) do
+ base_params.merge(
+ 'GitalyServer' => {
+ 'address' => Gitlab::GitalyClient.address(project.repository_storage),
+ 'token' => Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys
+ )
+ end
+
+ subject do
+ described_class.send_git_archive(repository, ref: ref, format: format)
+ end
+
+ context 'when Gitaly workhorse_archive feature is enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to include(gitaly_params)
+ end
+ end
+
+ context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq(base_params)
+ end
+ end
+
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
end
it "raises an error" do
- expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+ expect { subject }.to raise_error(RuntimeError)
end
end
end
@@ -182,7 +220,12 @@ describe Gitlab::Workhorse do
let(:repo_path) { repository.path_to_repo }
let(:action) { 'info_refs' }
let(:params) do
- { GL_ID: "user-#{user.id}", GL_REPOSITORY: "project-#{project.id}", RepoPath: repo_path }
+ {
+ GL_ID: "user-#{user.id}",
+ GL_USERNAME: user.username,
+ GL_REPOSITORY: "project-#{project.id}",
+ RepoPath: repo_path
+ }
end
subject { described_class.git_http_ok(repository, false, user, action) }
@@ -191,7 +234,12 @@ describe Gitlab::Workhorse do
context 'when is_wiki' do
let(:params) do
- { GL_ID: "user-#{user.id}", GL_REPOSITORY: "wiki-#{project.id}", RepoPath: repo_path }
+ {
+ GL_ID: "user-#{user.id}",
+ GL_USERNAME: user.username,
+ GL_REPOSITORY: "wiki-#{project.id}",
+ RepoPath: repo_path
+ }
end
subject { described_class.git_http_ok(repository, true, user, action) }
@@ -214,14 +262,13 @@ describe Gitlab::Workhorse do
end
it 'includes a Repository param' do
- repo_param = { Repository: {
+ repo_param = {
storage_name: 'default',
relative_path: project.full_path + '.git',
- git_object_directory: '',
- git_alternate_object_directories: []
- } }
+ gl_repository: "project-#{project.id}"
+ }
- expect(subject).to include(repo_param)
+ expect(subject[:Repository]).to include(repo_param)
end
context "when git_upload_pack action is passed" do
@@ -336,7 +383,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb
new file mode 100644
index 00000000000..87a3f43274f
--- /dev/null
+++ b/spec/lib/google_api/auth_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe GoogleApi::Auth do
+ let(:redirect_uri) { 'http://localhost:3000/google_api/authorizations/callback' }
+ let(:redirect_to) { 'http://localhost:3000/namaspace/project/clusters' }
+
+ let(:client) do
+ GoogleApi::CloudPlatform::Client
+ .new(nil, redirect_uri, state: redirect_to)
+ end
+
+ describe '#authorize_url' do
+ subject { client.authorize_url }
+
+ it 'returns authorize_url' do
+ is_expected.to start_with('https://accounts.google.com/o/oauth2')
+ is_expected.to include(URI.encode(redirect_uri, URI::PATTERN::RESERVED))
+ is_expected.to include(URI.encode(redirect_to, URI::PATTERN::RESERVED))
+ end
+ end
+
+ describe '#get_token' do
+ let(:token) do
+ double.tap do |dbl|
+ allow(dbl).to receive(:token).and_return('token')
+ allow(dbl).to receive(:expires_at).and_return('expires_at')
+ end
+ end
+
+ before do
+ allow_any_instance_of(OAuth2::Strategy::AuthCode)
+ .to receive(:get_token).and_return(token)
+ end
+
+ it 'returns token and expires_at' do
+ token, expires_at = client.get_token('xxx')
+ expect(token).to eq('token')
+ expect(expires_at).to eq('expires_at')
+ end
+ end
+end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
new file mode 100644
index 00000000000..acc5bd1da35
--- /dev/null
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe GoogleApi::CloudPlatform::Client do
+ let(:token) { 'token' }
+ let(:client) { described_class.new(token, nil) }
+
+ describe '.session_key_for_redirect_uri' do
+ let(:state) { 'random_string' }
+
+ subject { described_class.session_key_for_redirect_uri(state) }
+
+ it 'creates a new session key' do
+ is_expected.to eq('cloud_platform_second_redirect_uri_random_string')
+ end
+ end
+
+ describe '.new_session_key_for_redirect_uri' do
+ it 'generates a new session key' do
+ expect { |b| described_class.new_session_key_for_redirect_uri(&b) }
+ .to yield_with_args(String)
+ end
+ end
+
+ describe '#validate_token' do
+ subject { client.validate_token(expires_at) }
+
+ let(:expires_at) { 1.hour.since.utc.strftime('%s') }
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires_at is nil' do
+ let(:expires_at) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires in 1 hour' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when expires in 10 minutes' do
+ let(:expires_at) { 5.minutes.since.utc.strftime('%s') }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#projects_zones_clusters_get' do
+ subject { client.projects_zones_clusters_get(spy, spy, spy) }
+ let(:gke_cluster) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_cluster).and_return(gke_cluster)
+ end
+
+ it { is_expected.to eq(gke_cluster) }
+ end
+
+ describe '#projects_zones_clusters_create' do
+ subject do
+ client.projects_zones_clusters_create(
+ spy, spy, cluster_name, cluster_size, machine_type: machine_type)
+ end
+
+ let(:cluster_name) { 'test-cluster' }
+ let(:cluster_size) { 1 }
+ let(:machine_type) { 'n1-standard-4' }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:create_cluster).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+
+ it 'sets corresponded parameters' do
+ expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest)
+ .to receive(:initialize).with(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ subject
+ end
+ end
+
+ describe '#projects_zones_operations' do
+ subject { client.projects_zones_operations(spy, spy, spy) }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_operation).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+ end
+
+ describe '#parse_operation_id' do
+ subject { client.parse_operation_id(self_link) }
+
+ context 'when expected url' do
+ let(:self_link) do
+ 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123'
+ end
+
+ it { is_expected.to eq('ope-123') }
+ end
+
+ context 'when unexpected url' do
+ let(:self_link) { '???' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/config_spec.rb b/spec/lib/rspec_flaky/config_spec.rb
new file mode 100644
index 00000000000..83556787e85
--- /dev/null
+++ b/spec/lib/rspec_flaky/config_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe RspecFlaky::Config, :aggregate_failures do
+ before do
+ # Stub these env variables otherwise specs don't behave the same on the CI
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
+ end
+
+ describe '.generate_report?' do
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is not set" do
+ it 'returns false' do
+ expect(described_class).not_to be_generate_report
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'false'" do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
+ end
+
+ it 'returns false' do
+ expect(described_class).not_to be_generate_report
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'true'" do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
+ end
+
+ it 'returns true' do
+ expect(described_class).to be_generate_report
+ end
+ end
+ end
+
+ describe '.suite_flaky_examples_report_path' do
+ context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/suite-report.json')
+ .and_return('root/rspec_flaky/suite-report.json')
+
+ expect(described_class.suite_flaky_examples_report_path).to eq('root/rspec_flaky/suite-report.json')
+ end
+ end
+
+ context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.suite_flaky_examples_report_path).to eq('foo/suite-report.json')
+ end
+ end
+ end
+
+ describe '.flaky_examples_report_path' do
+ context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/report.json')
+ .and_return('root/rspec_flaky/report.json')
+
+ expect(described_class.flaky_examples_report_path).to eq('root/rspec_flaky/report.json')
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('FLAKY_RSPEC_REPORT_PATH', 'foo/report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.flaky_examples_report_path).to eq('foo/report.json')
+ end
+ end
+ end
+
+ describe '.new_flaky_examples_report_path' do
+ context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/new-report.json')
+ .and_return('root/rspec_flaky/new-report.json')
+
+ expect(described_class.new_flaky_examples_report_path).to eq('root/rspec_flaky/new-report.json')
+ end
+ end
+
+ context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', 'foo/new-report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.new_flaky_examples_report_path).to eq('foo/new-report.json')
+ end
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb
index cbfc1e538ab..d19c34bebb3 100644
--- a/spec/lib/rspec_flaky/flaky_example_spec.rb
+++ b/spec/lib/rspec_flaky/flaky_example_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe RspecFlaky::FlakyExample do
+describe RspecFlaky::FlakyExample, :aggregate_failures do
let(:flaky_example_attrs) do
{
example_id: 'spec/foo/bar_spec.rb:2',
@@ -9,6 +9,7 @@ describe RspecFlaky::FlakyExample do
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: 2345,
+ last_flaky_job: 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/12',
last_attempts_count: 2,
flaky_reports: 1
}
@@ -27,57 +28,78 @@ describe RspecFlaky::FlakyExample do
end
let(:example) { double(example_attrs) }
+ before do
+ # Stub these env variables otherwise specs don't behave the same on the CI
+ stub_env('CI_PROJECT_URL', nil)
+ stub_env('CI_JOB_ID', nil)
+ end
+
describe '#initialize' do
shared_examples 'a valid FlakyExample instance' do
- it 'returns valid attributes' do
- flaky_example = described_class.new(args)
+ let(:flaky_example) { described_class.new(args) }
+ it 'returns valid attributes' do
expect(flaky_example.uid).to eq(flaky_example_attrs[:uid])
- expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id])
+ expect(flaky_example.file).to eq(flaky_example_attrs[:file])
+ expect(flaky_example.line).to eq(flaky_example_attrs[:line])
+ expect(flaky_example.description).to eq(flaky_example_attrs[:description])
+ expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
+ expect(flaky_example.last_flaky_at).to eq(expected_last_flaky_at)
+ expect(flaky_example.last_attempts_count).to eq(flaky_example_attrs[:last_attempts_count])
+ expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
end
end
context 'when given an Rspec::Example' do
- let(:args) { example }
-
- it_behaves_like 'a valid FlakyExample instance'
+ it_behaves_like 'a valid FlakyExample instance' do
+ let(:args) { example }
+ let(:expected_first_flaky_at) { nil }
+ let(:expected_last_flaky_at) { nil }
+ let(:expected_flaky_reports) { 0 }
+ end
end
context 'when given a hash' do
- let(:args) { flaky_example_attrs }
-
- it_behaves_like 'a valid FlakyExample instance'
+ it_behaves_like 'a valid FlakyExample instance' do
+ let(:args) { flaky_example_attrs }
+ let(:expected_flaky_reports) { flaky_example_attrs[:flaky_reports] }
+ let(:expected_first_flaky_at) { flaky_example_attrs[:first_flaky_at] }
+ let(:expected_last_flaky_at) { flaky_example_attrs[:last_flaky_at] }
+ end
end
end
- describe '#to_h' do
- before do
- # Stub these env variables otherwise specs don't behave the same on the CI
- stub_env('CI_PROJECT_URL', nil)
- stub_env('CI_JOB_ID', nil)
- end
+ describe '#update_flakiness!' do
+ shared_examples 'an up-to-date FlakyExample instance' do
+ let(:flaky_example) { described_class.new(args) }
- shared_examples 'a valid FlakyExample hash' do
- let(:additional_attrs) { {} }
+ it 'updates the first_flaky_at' do
+ now = Time.now
+ expected_first_flaky_at = flaky_example.first_flaky_at ? flaky_example.first_flaky_at : now
+ Timecop.freeze(now) { flaky_example.update_flakiness! }
- it 'returns a valid hash' do
- flaky_example = described_class.new(args)
- final_hash = flaky_example_attrs
- .merge(last_flaky_at: instance_of(Time), last_flaky_job: nil)
- .merge(additional_attrs)
+ expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
+ end
+
+ it 'updates the last_flaky_at' do
+ now = Time.now
+ Timecop.freeze(now) { flaky_example.update_flakiness! }
- expect(flaky_example.to_h).to match(hash_including(final_hash))
+ expect(flaky_example.last_flaky_at).to eq(now)
end
- end
- context 'when given an Rspec::Example' do
- let(:args) { example }
+ it 'updates the flaky_reports' do
+ expected_flaky_reports = flaky_example.first_flaky_at ? flaky_example.flaky_reports + 1 : 1
+
+ expect { flaky_example.update_flakiness! }.to change { flaky_example.flaky_reports }.by(1)
+ expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
+ end
+
+ context 'when passed a :last_attempts_count' do
+ it 'updates the last_attempts_count' do
+ flaky_example.update_flakiness!(last_attempts_count: 42)
- context 'when run locally' do
- it_behaves_like 'a valid FlakyExample hash' do
- let(:additional_attrs) do
- { first_flaky_at: instance_of(Time) }
- end
+ expect(flaky_example.last_attempts_count).to eq(42)
end
end
@@ -87,10 +109,45 @@ describe RspecFlaky::FlakyExample do
stub_env('CI_JOB_ID', 42)
end
- it_behaves_like 'a valid FlakyExample hash' do
- let(:additional_attrs) do
- { first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" }
- end
+ it 'updates the last_flaky_job' do
+ flaky_example.update_flakiness!
+
+ expect(flaky_example.last_flaky_job).to eq('https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42')
+ end
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ it_behaves_like 'an up-to-date FlakyExample instance' do
+ let(:args) { example }
+ end
+ end
+
+ context 'when given a hash' do
+ it_behaves_like 'an up-to-date FlakyExample instance' do
+ let(:args) { flaky_example_attrs }
+ end
+ end
+ end
+
+ describe '#to_h' do
+ shared_examples 'a valid FlakyExample hash' do
+ let(:additional_attrs) { {} }
+
+ it 'returns a valid hash' do
+ flaky_example = described_class.new(args)
+ final_hash = flaky_example_attrs.merge(additional_attrs)
+
+ expect(flaky_example.to_h).to eq(final_hash)
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ let(:args) { example }
+
+ it_behaves_like 'a valid FlakyExample hash' do
+ let(:additional_attrs) do
+ { first_flaky_at: nil, last_flaky_at: nil, last_flaky_job: nil, flaky_reports: 0 }
end
end
end
diff --git a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
new file mode 100644
index 00000000000..06a8ba0d02e
--- /dev/null
+++ b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
+ let(:collection_hash) do
+ {
+ a: { example_id: 'spec/foo/bar_spec.rb:2' },
+ b: { example_id: 'spec/foo/baz_spec.rb:3' }
+ }
+ end
+ let(:collection_report) do
+ {
+ a: {
+ example_id: 'spec/foo/bar_spec.rb:2',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ },
+ b: {
+ example_id: 'spec/foo/baz_spec.rb:3',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ }
+ }
+ end
+
+ describe '.from_json' do
+ it 'accepts a JSON' do
+ collection = described_class.from_json(JSON.pretty_generate(collection_hash))
+
+ expect(collection.to_report).to eq(described_class.new(collection_hash).to_report)
+ end
+ end
+
+ describe '#initialize' do
+ it 'accepts no argument' do
+ expect { described_class.new }.not_to raise_error
+ end
+
+ it 'accepts a hash' do
+ expect { described_class.new(collection_hash) }.not_to raise_error
+ end
+
+ it 'does not accept anything else' do
+ expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`collection` must be a Hash, Array given!")
+ end
+ end
+
+ describe '#to_report' do
+ it 'calls #to_h on the values' do
+ collection = described_class.new(collection_hash)
+
+ expect(collection.to_report).to eq(collection_report)
+ end
+ end
+
+ describe '#-' do
+ it 'returns only examples that are not present in the given collection' do
+ collection1 = described_class.new(collection_hash)
+ collection2 = described_class.new(
+ a: { example_id: 'spec/foo/bar_spec.rb:2' },
+ c: { example_id: 'spec/bar/baz_spec.rb:4' })
+
+ expect((collection2 - collection1).to_report).to eq(
+ c: {
+ example_id: 'spec/bar/baz_spec.rb:4',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ })
+ end
+
+ it 'fails if the given collection does not respond to `#key?`' do
+ collection = described_class.new(collection_hash)
+
+ expect { collection - [1, 2, 3] }.to raise_error(ArgumentError, "`other` must respond to `#key?`, Array does not!")
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb
index 0e193bf408b..bfb7648b486 100644
--- a/spec/lib/rspec_flaky/listener_spec.rb
+++ b/spec/lib/rspec_flaky/listener_spec.rb
@@ -1,22 +1,35 @@
require 'spec_helper'
-describe RspecFlaky::Listener do
- let(:flaky_example_report) do
+describe RspecFlaky::Listener, :aggregate_failures do
+ let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' }
+ let(:suite_flaky_example_report) do
{
- 'abc123' => {
+ already_flaky_example_uid => {
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
- last_flaky_at: instance_of(Time),
- last_attempts_count: 2,
+ last_flaky_at: 4321,
+ last_attempts_count: 3,
flaky_reports: 1,
last_flaky_job: nil
}
}
end
- let(:example_attrs) do
+ let(:already_flaky_example_attrs) do
+ {
+ id: 'spec/foo/bar_spec.rb:2',
+ metadata: {
+ file_path: 'spec/foo/bar_spec.rb',
+ line_number: 2,
+ full_description: 'hello world'
+ },
+ execution_result: double(status: 'passed', exception: nil)
+ }
+ end
+ let(:already_flaky_example) { RspecFlaky::FlakyExample.new(suite_flaky_example_report[already_flaky_example_uid]) }
+ let(:new_example_attrs) do
{
id: 'spec/foo/baz_spec.rb:3',
metadata: {
@@ -32,18 +45,19 @@ describe RspecFlaky::Listener do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
end
describe '#initialize' do
shared_examples 'a valid Listener instance' do
- let(:expected_all_flaky_examples) { {} }
+ let(:expected_suite_flaky_examples) { {} }
it 'returns a valid Listener instance' do
listener = described_class.new
- expect(listener.to_report(listener.all_flaky_examples))
- .to match(hash_including(expected_all_flaky_examples))
- expect(listener.new_flaky_examples).to eq({})
+ expect(listener.to_report(listener.suite_flaky_examples))
+ .to eq(expected_suite_flaky_examples)
+ expect(listener.flaky_examples).to eq({})
end
end
@@ -51,16 +65,16 @@ describe RspecFlaky::Listener do
it_behaves_like 'a valid Listener instance'
end
- context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do
+ context 'when a report file exists and set by SUITE_FLAKY_RSPEC_REPORT_PATH' do
let(:report_file) do
Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
- f.write(JSON.pretty_generate(flaky_example_report))
+ f.write(JSON.pretty_generate(suite_flaky_example_report))
f.rewind
end
end
before do
- stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file.path)
end
after do
@@ -69,74 +83,122 @@ describe RspecFlaky::Listener do
end
it_behaves_like 'a valid Listener instance' do
- let(:expected_all_flaky_examples) { flaky_example_report }
+ let(:expected_suite_flaky_examples) { suite_flaky_example_report }
end
end
end
describe '#example_passed' do
- let(:rspec_example) { double(example_attrs) }
+ let(:rspec_example) { double(new_example_attrs) }
let(:notification) { double(example: rspec_example) }
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
shared_examples 'a non-flaky example' do
it 'does not change the flaky examples hash' do
- expect { subject.example_passed(notification) }
- .not_to change { subject.all_flaky_examples }
+ expect { listener.example_passed(notification) }
+ .not_to change { listener.flaky_examples }
end
end
- describe 'when the RSpec example does not respond to attempts' do
- it_behaves_like 'a non-flaky example'
- end
+ shared_examples 'an existing flaky example' do
+ let(:expected_flaky_example) do
+ {
+ example_id: 'spec/foo/bar_spec.rb:2',
+ file: 'spec/foo/bar_spec.rb',
+ line: 2,
+ description: 'hello world',
+ first_flaky_at: 1234,
+ last_attempts_count: 2,
+ flaky_reports: 2,
+ last_flaky_job: nil
+ }
+ end
- describe 'when the RSpec example has 1 attempt' do
- let(:rspec_example) { double(example_attrs.merge(attempts: 1)) }
+ it 'changes the flaky examples hash' do
+ new_example = RspecFlaky::Example.new(rspec_example)
- it_behaves_like 'a non-flaky example'
+ now = Time.now
+ Timecop.freeze(now) do
+ expect { listener.example_passed(notification) }
+ .to change { listener.flaky_examples[new_example.uid].to_h }
+ end
+
+ expect(listener.flaky_examples[new_example.uid].to_h)
+ .to eq(expected_flaky_example.merge(last_flaky_at: now))
+ end
end
- describe 'when the RSpec example has 2 attempts' do
- let(:rspec_example) { double(example_attrs.merge(attempts: 2)) }
- let(:expected_new_flaky_example) do
+ shared_examples 'a new flaky example' do
+ let(:expected_flaky_example) do
{
example_id: 'spec/foo/baz_spec.rb:3',
file: 'spec/foo/baz_spec.rb',
line: 3,
description: 'hello GitLab',
- first_flaky_at: instance_of(Time),
- last_flaky_at: instance_of(Time),
last_attempts_count: 2,
flaky_reports: 1,
last_flaky_job: nil
}
end
- it 'does not change the flaky examples hash' do
- expect { subject.example_passed(notification) }
- .to change { subject.all_flaky_examples }
-
+ it 'changes the all flaky examples hash' do
new_example = RspecFlaky::Example.new(rspec_example)
- expect(subject.all_flaky_examples[new_example.uid].to_h)
- .to match(hash_including(expected_new_flaky_example))
+ now = Time.now
+ Timecop.freeze(now) do
+ expect { listener.example_passed(notification) }
+ .to change { listener.flaky_examples[new_example.uid].to_h }
+ end
+
+ expect(listener.flaky_examples[new_example.uid].to_h)
+ .to eq(expected_flaky_example.merge(first_flaky_at: now, last_flaky_at: now))
+ end
+ end
+
+ describe 'when the RSpec example does not respond to attempts' do
+ it_behaves_like 'a non-flaky example'
+ end
+
+ describe 'when the RSpec example has 1 attempt' do
+ let(:rspec_example) { double(new_example_attrs.merge(attempts: 1)) }
+
+ it_behaves_like 'a non-flaky example'
+ end
+
+ describe 'when the RSpec example has 2 attempts' do
+ let(:rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
+
+ it_behaves_like 'a new flaky example'
+
+ context 'with an existing flaky example' do
+ let(:rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
+
+ it_behaves_like 'an existing flaky example'
end
end
end
describe '#dump_summary' do
- let(:rspec_example) { double(example_attrs) }
- let(:notification) { double(example: rspec_example) }
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
+ let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
+ let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
+ let(:notification_new_flaky_rspec_example) { double(example: new_flaky_rspec_example) }
+ let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) }
- context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do
+ context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
+ let(:new_report_file_path) { Rails.root.join('tmp', 'rspec_flaky_new_report.json') }
before do
- stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ stub_env('FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', new_report_file_path)
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
after do
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
@@ -144,12 +206,13 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
end
- it 'does not write the report file' do
- subject.example_passed(notification)
+ it 'does not write any report file' do
+ listener.example_passed(notification_new_flaky_rspec_example)
- subject.dump_summary(nil)
+ listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(false)
+ expect(File.exist?(new_report_file_path)).to be(false)
end
end
@@ -158,21 +221,39 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
end
- it 'writes the report file' do
- subject.example_passed(notification)
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'writes the report files' do
+ listener.example_passed(notification_new_flaky_rspec_example)
+ listener.example_passed(notification_already_flaky_rspec_example)
- subject.dump_summary(nil)
+ listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(true)
+ expect(File.exist?(new_report_file_path)).to be(true)
+
+ expect(File.read(report_file_path))
+ .to eq(JSON.pretty_generate(listener.to_report(listener.flaky_examples)))
+
+ new_example = RspecFlaky::Example.new(notification_new_flaky_rspec_example)
+ new_flaky_example = RspecFlaky::FlakyExample.new(new_example)
+ new_flaky_example.update_flakiness!
+
+ expect(File.read(new_report_file_path))
+ .to eq(JSON.pretty_generate(listener.to_report(new_example.uid => new_flaky_example)))
end
end
end
end
describe '#to_report' do
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
+
it 'transforms the internal hash to a JSON-ready hash' do
- expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123'])))
- .to match(hash_including(flaky_example_report))
+ expect(listener.to_report(already_flaky_example_uid => already_flaky_example))
+ .to match(hash_including(suite_flaky_example_report))
end
end
end
diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
index 7125bfcab59..b4b83b70d1c 100644
--- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -16,7 +16,12 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
end
it 'only whitelists safe files' do
- expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts')
+ expect(described_class::WHITELIST).to contain_exactly(
+ 'authorized_keys',
+ 'authorized_keys2',
+ 'authorized_keys.lock',
+ 'known_hosts'
+ )
end
describe '#skip?' do
@@ -34,6 +39,14 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) }
end
+
+ it 'skips GitLab read-only instances' do
+ stub_user
+ stub_home_dir
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ is_expected.to be_truthy
+ end
end
describe '#check?' do
diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb
new file mode 100644
index 00000000000..faf8c99e772
--- /dev/null
+++ b/spec/lib/system_check/base_check_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe SystemCheck::BaseCheck do
+ context 'helpers on instance level' do
+ it 'responds to SystemCheck::Helpers methods' do
+ expect(subject).to respond_to :fix_and_rerun, :for_more_information, :see_installation_guide_section,
+ :finished_checking, :start_checking, :try_fixing_it, :sanitized_message, :should_sanitize?, :omnibus_gitlab?,
+ :sudo_gitlab
+ end
+
+ it 'responds to Gitlab::TaskHelpers methods' do
+ expect(subject).to respond_to :ask_to_continue, :os_name, :prompt, :run_and_match, :run_command,
+ :run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab, :all_repos,
+ :repository_storage_paths_args, :user_home, :checkout_or_clone_version, :clone_repo, :checkout_version
+ end
+ end
+end
diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb
new file mode 100644
index 00000000000..2a61ff3ad65
--- /dev/null
+++ b/spec/lib/system_check/orphans/namespace_check_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::Orphans::NamespaceCheck do
+ let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } }
+
+ before do
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces)
+ silence_output
+ end
+
+ describe '#multi_check' do
+ context 'all orphans' do
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
+
+ it 'prints list of all orphaned namespaces except @hashed' do
+ expect_list_of_orphans(%w(orphan1 orphan2))
+
+ subject.multi_check
+ end
+ end
+
+ context 'few orphans with existing namespace' do
+ let!(:first_level) { create(:group, path: 'my-namespace') }
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
+
+ it 'prints list of orphaned namespaces' do
+ expect_list_of_orphans(%w(orphan1 orphan2))
+
+ subject.multi_check
+ end
+ end
+
+ context 'few orphans with existing namespace and parents with same name as orphans' do
+ let!(:first_level) { create(:group, path: 'my-namespace') }
+ let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
+
+ it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
+ expect_list_of_orphans(%w(orphan1 orphan2 second-level))
+
+ subject.multi_check
+ end
+ end
+
+ context 'no orphans' do
+ let(:disk_namespaces) { %w(@hashed) }
+
+ it 'prints an empty list ignoring @hashed' do
+ expect_list_of_orphans([])
+
+ subject.multi_check
+ end
+ end
+ end
+
+ def expect_list_of_orphans(orphans)
+ expect(subject).to receive(:print_orphans).with(orphans, 'default')
+ end
+end
diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb
new file mode 100644
index 00000000000..b0c2267d177
--- /dev/null
+++ b/spec/lib/system_check/orphans/repository_check_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::Orphans::RepositoryCheck do
+ let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } }
+
+ before do
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces)
+ allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories)
+ # silence_output
+ end
+
+ describe '#multi_check' do
+ context 'all orphans' do
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
+ let(:disk_repositories) { %w(repo1.git repo2.git) }
+
+ it 'prints list of all orphaned namespaces except @hashed' do
+ expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git))
+
+ subject.multi_check
+ end
+ end
+
+ context 'few orphans with existing namespace' do
+ let!(:first_level) { create(:group, path: 'my-namespace') }
+ let!(:project) { create(:project, path: 'repo', namespace: first_level) }
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
+ let(:disk_repositories) { %w(repo.git) }
+
+ it 'prints list of orphaned namespaces' do
+ expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git))
+
+ subject.multi_check
+ end
+ end
+
+ context 'few orphans with existing namespace and parents with same name as orphans' do
+ let!(:first_level) { create(:group, path: 'my-namespace') }
+ let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
+ let!(:project) { create(:project, path: 'repo', namespace: first_level) }
+ let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
+ let(:disk_repositories) { %w(repo.git) }
+
+ it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
+ expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git))
+
+ subject.multi_check
+ end
+ end
+
+ context 'no orphans' do
+ let(:disk_namespaces) { %w(@hashed) }
+ let(:disk_repositories) { %w(repo.git) }
+
+ it 'prints an empty list ignoring @hashed' do
+ expect_list_of_orphans([])
+
+ subject.multi_check
+ end
+ end
+ end
+
+ def expect_list_of_orphans(orphans)
+ expect(subject).to receive(:print_orphans).with(orphans, 'default')
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 09e5094cf84..1f7be415e35 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -120,29 +120,4 @@ describe Emails::Profile do
it { expect { Notify.new_gpg_key_email('foo') }.not_to raise_error }
end
end
-
- describe 'user added email' do
- let(:email) { create(:email) }
-
- subject { Notify.new_email_email(email.id) }
-
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
-
- it 'is sent to the new user' do
- is_expected.to deliver_to email.user.email
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /^Email was added to your account$/i
- end
-
- it 'contains the new email address' do
- is_expected.to have_body_text /#{email.email}/
- end
-
- it 'includes a link to emails page' do
- is_expected.to have_body_text /#{profile_emails_path}/
- end
- end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 932e2fd8c95..c832cee965b 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -28,319 +28,334 @@ describe Notify do
end
def have_referable_subject(referable, reply: false)
- prefix = referable.project.name if referable.project
- prefix = "Re: #{prefix}" if reply
+ prefix = referable.project ? "#{referable.project.name} | " : ''
+ prefix.prepend('Re: ') if reply
suffix = "#{referable.title} (#{referable.to_reference})"
- have_subject [prefix, suffix].compact.join(' | ')
+ have_subject [prefix, suffix].compact.join
end
context 'for a project' do
- describe 'items that are assignable, the email' do
- let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ shared_examples 'an assignee email' do
+ it 'is sent to the assignee as the author' do
+ sender = subject.header[:from].addrs.first
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(assignee.email)
+ end
+ end
+ end
- shared_examples 'an assignee email' do
- it 'is sent to the assignee as the author' do
- sender = subject.header[:from].addrs.first
+ context 'for issues' do
+ describe 'that are new' do
+ subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct subject and body' do
aggregate_failures do
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(assignee.email)
+ is_expected.to have_referable_subject(issue)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
- end
- context 'for issues' do
- describe 'that are new' do
- subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text issue.description
+ end
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text issue.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text(issue.author_name)
+ is_expected.to have_body_text 'created an issue:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text(issue.author_name)
- is_expected.to have_body_text 'created an issue:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- describe 'that have been reassigned' do
- subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_html_escaped_body_text(assignee.name)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_html_escaped_body_text(assignee.name)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
+ context 'with a preferred language' do
+ before do
+ Gitlab::I18n.locale = :es
end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ after do
+ Gitlab::I18n.use_default_locale
end
- context 'with a preferred language' do
- before do
- Gitlab::I18n.locale = :es
- end
-
- after do
- Gitlab::I18n.use_default_locale
- end
-
- it 'always generates the email using the default language' do
- is_expected.to have_body_text('foo, bar, and baz')
- end
+ it 'always generates the email using the default language' do
+ is_expected.to have_body_text('foo, bar, and baz')
end
end
+ end
- describe 'status changed' do
- let(:status) { 'closed' }
- subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
+ describe 'status changed' do
+ let(:status) { 'closed' }
+ subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_issue_path project, issue)
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
+ is_expected.to have_body_text(project_issue_path project, issue)
end
end
+ end
- describe 'moved to another project' do
- let(:new_issue) { create(:issue) }
- subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
+ describe 'moved to another project' do
+ let(:new_issue) { create(:issue) }
+ subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'contains description about action taken' do
- is_expected.to have_body_text 'Issue was moved to another project'
- end
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
- it 'has the correct subject and body' do
- new_issue_url = project_issue_path(new_issue.project, new_issue)
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(new_issue_url)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
end
+ end
- context 'for merge requests' do
- describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ context 'for merge requests' do
+ describe 'that are new' do
+ subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_body_text(merge_request.source_branch)
+ is_expected.to have_body_text(merge_request.target_branch)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_body_text(merge_request.source_branch)
- is_expected.to have_body_text(merge_request.target_branch)
- end
+ end
+
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text merge_request.description
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text merge_request.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text merge_request.author_name
+ is_expected.to have_body_text 'created a merge request:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text merge_request.author_name
- is_expected.to have_body_text 'created a merge request:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
- describe 'that are reassigned' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_html_escaped_body_text(assignee.name)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_html_escaped_body_text(assignee.name)
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'status changed' do
+ let(:status) { 'reopened' }
+ subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
- it 'has the correct subject and body' do
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
- describe 'status changed' do
- let(:status) { 'reopened' }
- subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
+ describe 'that are merged' do
+ let(:merge_author) { create(:user) }
+ subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the merge author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('merged')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
+ end
- describe 'that are merged' do
- let(:merge_author) { create(:user) }
- subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
+ context 'for snippet notes' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
- it 'is sent as the merge author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(merge_author.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { project_snippet }
+ end
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('merged')
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
- end
- end
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(project_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text project_snippet_note.note
end
end
@@ -1239,4 +1254,18 @@ describe Notify do
end
end
end
+
+ context 'for personal snippet notes' do
+ let(:personal_snippet) { create(:personal_snippet) }
+ let(:personal_snippet_note) { create(:note_on_personal_snippet, noteable: personal_snippet) }
+
+ subject { described_class.note_personal_snippet_email(personal_snippet_note.author_id, personal_snippet_note.id) }
+
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(personal_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text personal_snippet_note.note
+ end
+ end
end
diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
index 862907c5d01..84c2e9f7e52 100644
--- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
+++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
@@ -2,11 +2,12 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
describe AddHeadPipelineForEachMergeRequest, :truncate do
+ include ProjectForksHelper
+
let(:migration) { described_class.new }
let!(:project) { create(:project) }
- let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
- let!(:other_project) { forked_project_link.forked_to_project }
+ let!(:other_project) { fork_project(project) }
let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") }
let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") }
diff --git a/spec/migrations/clean_stages_statuses_migration_spec.rb b/spec/migrations/clean_stages_statuses_migration_spec.rb
new file mode 100644
index 00000000000..38705f8eaae
--- /dev/null
+++ b/spec/migrations/clean_stages_statuses_migration_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170912113435_clean_stages_statuses_migration.rb')
+
+describe CleanStagesStatusesMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateStageStatus)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateStageStatus', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateStageStatus', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated stages afterwards' do
+ let(:stages) { table('ci_stages') }
+
+ before do
+ stages.create!(status: nil, name: 'build')
+ stages.create!(status: nil, name: 'test')
+ end
+
+ it 'migrates statuses sequentially in batches' do
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
new file mode 100644
index 00000000000..1df2477da51
--- /dev/null
+++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170907170235_delete_conflicting_redirect_routes')
+
+describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2)
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
+
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect do
+ migrate!
+ end.to change { redirect_routes.count }.from(8).to(3)
+ end
+ end
+end
diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb
new file mode 100644
index 00000000000..262d7742aaf
--- /dev/null
+++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170921101004_normalize_ldap_extern_uids')
+
+describe NormalizeLdapExternUids, :migration, :sidekiq do
+ let!(:identities) { table(:identities) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_BATCH_SIZE", 2)
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
+
+ # LDAP identities
+ (1..4).each do |i|
+ identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i)
+ end
+
+ # Non-LDAP identity
+ identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'migrates the LDAP identities' do
+ Sidekiq::Testing.inline! do
+ migrate!
+ identities.where(id: 1..4).each do |identity|
+ expect(identity.extern_uid).to eq("uid=foo #{identity.id},ou=people,dc=example,dc=com")
+ end
+ end
+ end
+
+ it 'does not modify non-LDAP identities' do
+ Sidekiq::Testing.inline! do
+ migrate!
+ identity = identities.last
+ expect(identity.extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ end
+ end
+end
diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
new file mode 100644
index 00000000000..0e884a7d910
--- /dev/null
+++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys')
+
+describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do
+ matcher :be_scheduled_migration do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ before do
+ create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key)
+ create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key)
+ # Delete all subkeys so they can be recreated
+ GpgKeySubkey.destroy_all
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(2)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect(GpgKeySubkey.count).to eq(0)
+
+ migrate!
+
+ expect(GpgKeySubkey.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
new file mode 100644
index 00000000000..4ab1bb67058
--- /dev/null
+++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two')
+
+describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
+ matcher :be_scheduled_migration do |time, *expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected] &&
+ job['at'].to_i == time.to_i
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ let(:merge_request_diffs) { table(:merge_request_diffs) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:projects) { table(:projects) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ projects.create!(id: 1, name: 'gitlab', path: 'gitlab')
+
+ merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master')
+
+ merge_request_diffs.create!(id: 1, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: nil)
+ merge_request_diffs.create!(id: 2, merge_request_id: 1, st_commits: nil, st_diffs: YAML.dump([]))
+ merge_request_diffs.create!(id: 3, merge_request_id: 1, st_commits: nil, st_diffs: nil)
+ merge_request_diffs.create!(id: 4, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: YAML.dump([]))
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'migrates the data' do
+ Sidekiq::Testing.inline! do
+ non_empty = 'st_commits IS NOT NULL OR st_diffs IS NOT NULL'
+
+ expect(merge_request_diffs.where(non_empty).count).to eq 3
+
+ migrate!
+
+ expect(merge_request_diffs.where(non_empty).count).to eq 0
+ end
+ end
+end
diff --git a/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb b/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb
new file mode 100644
index 00000000000..d625b60ff50
--- /dev/null
+++ b/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170927112318_update_legacy_diff_notes_type_for_import.rb')
+
+describe UpdateLegacyDiffNotesTypeForImport, :migration do
+ let(:notes) { table(:notes) }
+
+ before do
+ notes.inheritance_column = nil
+
+ notes.create(type: 'Note')
+ notes.create(type: 'LegacyDiffNote')
+ notes.create(type: 'Github::Import::Note')
+ notes.create(type: 'Github::Import::LegacyDiffNote')
+ end
+
+ it 'updates the notes type' do
+ migrate!
+
+ expect(notes.pluck(:type))
+ .to contain_exactly('Note', 'Github::Import::Note', 'LegacyDiffNote', 'LegacyDiffNote')
+ end
+end
diff --git a/spec/migrations/update_notes_type_for_import_spec.rb b/spec/migrations/update_notes_type_for_import_spec.rb
new file mode 100644
index 00000000000..06195d970d8
--- /dev/null
+++ b/spec/migrations/update_notes_type_for_import_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170927112319_update_notes_type_for_import.rb')
+
+describe UpdateNotesTypeForImport, :migration do
+ let(:notes) { table(:notes) }
+
+ before do
+ notes.inheritance_column = nil
+
+ notes.create(type: 'Note')
+ notes.create(type: 'LegacyDiffNote')
+ notes.create(type: 'Github::Import::Note')
+ notes.create(type: 'Github::Import::LegacyDiffNote')
+ end
+
+ it 'updates the notes type' do
+ migrate!
+
+ expect(notes.pluck(:type))
+ .to contain_exactly('Note', 'Note', 'LegacyDiffNote', 'Github::Import::LegacyDiffNote')
+ end
+end
diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb
index 0a45c5ea32d..d4a1553fb0e 100644
--- a/spec/migrations/update_upload_paths_to_system_spec.rb
+++ b/spec/migrations/update_upload_paths_to_system_spec.rb
@@ -31,7 +31,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#up", truncate: true do
+ describe "#up", :truncate do
it "updates old upload records to the new path" do
old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg")
@@ -41,7 +41,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#down", truncate: true do
+ describe "#down", :truncate do
it "updates the new system patsh to the old paths" do
new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d4da30b1641..f49a61062c1 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -1,8 +1,9 @@
require 'rails_helper'
-RSpec.describe AbuseReport do
- subject { create(:abuse_report) }
- let(:user) { create(:admin) }
+describe AbuseReport do
+ set(:report) { create(:abuse_report) }
+ set(:user) { create(:admin) }
+ subject { report }
it { expect(subject).to be_valid }
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index b5d5d58697b..49f44525b29 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe Appearance do
+describe Appearance do
subject { build(:appearance) }
it { is_expected.to be_valid }
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c7a9eabdf06..78cacf9ff5d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -167,19 +167,33 @@ describe ApplicationSetting do
context 'housekeeping settings' do
it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) }
- it 'wants the full repack period to be longer than the incremental repack period' do
+ it 'wants the full repack period to be at least the incremental repack period' do
subject.housekeeping_incremental_repack_period = 2
subject.housekeeping_full_repack_period = 1
expect(subject).not_to be_valid
end
- it 'wants the gc period to be longer than the full repack period' do
- subject.housekeeping_full_repack_period = 2
- subject.housekeeping_gc_period = 1
+ it 'wants the gc period to be at least the full repack period' do
+ subject.housekeeping_full_repack_period = 100
+ subject.housekeeping_gc_period = 90
expect(subject).not_to be_valid
end
+
+ it 'allows the same period for incremental repack and full repack, effectively skipping incremental repack' do
+ subject.housekeeping_incremental_repack_period = 2
+ subject.housekeeping_full_repack_period = 2
+
+ expect(subject).to be_valid
+ end
+
+ it 'allows the same period for full repack and gc, effectively skipping full repack' do
+ subject.housekeeping_full_repack_period = 100
+ subject.housekeeping_gc_period = 100
+
+ expect(subject).to be_valid
+ end
end
end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 8581bcbb08b..e89e534d914 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe ChatName do
- subject { create(:chat_name) }
+ set(:chat_name) { create(:chat_name) }
+ subject { chat_name }
it { is_expected.to belong_to(:service) }
it { is_expected.to belong_to(:user) }
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
index e0e5f73e6fe..70a9a206faa 100644
--- a/spec/models/chat_team_spec.rb
+++ b/spec/models/chat_team_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe ChatTeam do
- subject { create(:chat_team) }
+ set(:chat_team) { create(:chat_team) }
+ subject { chat_team }
# Associations
it { is_expected.to belong_to(:namespace) }
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index a10a8af5303..d5ba088af53 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Ci::ArtifactBlob do
- let(:build) { create(:ci_build, :artifacts) }
+ set(:project) { create(:project, :public) }
+ set(:build) { create(:ci_build, :artifacts, project: project) }
let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') }
subject { described_class.new(entry) }
@@ -41,4 +42,51 @@ describe Ci::ArtifactBlob do
expect(subject.external_storage).to eq(:build_artifact)
end
end
+
+ describe '#external_url' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context '.gif extension' do
+ it 'returns nil' do
+ expect(subject.external_url(build.project, build)).to be_nil
+ end
+ end
+
+ context 'txt extensions' do
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ it 'returns a URL' do
+ url = subject.external_url(build.project, build)
+
+ expect(url).not_to be_nil
+ expect(url).to start_with("http")
+ expect(url).to match Gitlab.config.pages.host
+ expect(url).to end_with(entry.path)
+ end
+ end
+ end
+
+ describe '#external_link?' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'gif extensions' do
+ it 'returns false' do
+ expect(subject.external_link?(build)).to be false
+ end
+ end
+
+ context 'txt extensions' do
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ it 'returns true' do
+ expect(subject.external_link?(build)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index d67f578ab8a..1e3da4e1f1a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,22 +1,24 @@
require 'spec_helper'
describe Ci::Build do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:test_trace) { 'This is a test' }
+ set(:user) { create(:user) }
+ set(:group) { create(:group, :access_requestable) }
+ set(:project) { create(:project, :repository, group: group) }
- let(:pipeline) do
+ set(:pipeline) do
create(:ci_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success')
end
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to have_many(:deployments) }
+ it { is_expected.to have_many(:trace_sections)}
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
@@ -326,7 +328,7 @@ describe Ci::Build do
let(:project_regex) { '\(\d+\.\d+\) covered' }
before do
- project.build_coverage_regex = project_regex
+ project.update_column(:build_coverage_regex, project_regex)
end
context 'and coverage_regex attribute is not set' do
@@ -363,6 +365,17 @@ describe Ci::Build do
end
end
+ describe '#parse_trace_sections!' do
+ it 'calls ExtractSectionsFromBuildTraceService' do
+ expect(Ci::ExtractSectionsFromBuildTraceService)
+ .to receive(:new).with(project, build.user).once.and_call_original
+ expect_any_instance_of(Ci::ExtractSectionsFromBuildTraceService)
+ .to receive(:execute).with(build).once
+
+ build.parse_trace_sections!
+ end
+ end
+
describe '#trace' do
subject { build.trace }
@@ -1140,9 +1153,6 @@ describe Ci::Build do
end
describe '#repo_url' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
subject { build.repo_url }
it { is_expected.to be_a(String) }
@@ -1243,6 +1253,8 @@ describe Ci::Build do
end
context 'use from gitlab-ci.yml' do
+ let(:pipeline) { create(:ci_pipeline) }
+
before do
stub_ci_pipeline_yaml_file(config)
end
@@ -1486,11 +1498,7 @@ describe Ci::Build do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
- let(:group) { create(:group, :access_requestable) }
-
before do
- build.project.update(group: group)
-
create(:ci_group_variable,
secret_variable.slice(:key, :value).merge(group: group))
end
@@ -1503,11 +1511,7 @@ describe Ci::Build do
{ key: 'PROTECTED_KEY', value: 'protected_value', public: false }
end
- let(:group) { create(:group, :access_requestable) }
-
before do
- build.project.update(group: group)
-
create(:ci_group_variable,
:protected,
protected_variable.slice(:key, :value).merge(group: group))
@@ -1530,6 +1534,10 @@ describe Ci::Build do
end
context 'when the ref is not protected' do
+ before do
+ build.update_column(:ref, 'some/feature')
+ end
+
it { is_expected.not_to include(protected_variable) }
end
end
@@ -1596,6 +1604,8 @@ describe Ci::Build do
end
context 'when yaml_variables are undefined' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
before do
build.yaml_variables = nil
end
@@ -1689,7 +1699,10 @@ describe Ci::Build do
before do
build.environment = 'production'
- allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+
+ allow_any_instance_of(Project)
+ .to receive(:deployment_variables)
+ .and_return([deployment_variable])
end
it { is_expected.to include(deployment_variable) }
@@ -1713,14 +1726,19 @@ describe Ci::Build do
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
- allow(project).to receive(:predefined_variables) { [project_pre_var] }
- allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
- allow(project).to receive(:secret_variables_for)
+ allow_any_instance_of(Project)
+ .to receive(:predefined_variables) { [project_pre_var] }
+
+ allow_any_instance_of(Project)
+ .to receive(:secret_variables_for)
.with(ref: 'master', environment: nil) do
[create(:ci_variable, key: 'secret', value: 'value')]
end
+
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:predefined_variables) { [pipeline_pre_var] }
end
it do
diff --git a/spec/models/ci/build_trace_section_name_spec.rb b/spec/models/ci/build_trace_section_name_spec.rb
new file mode 100644
index 00000000000..386ee6880cb
--- /dev/null
+++ b/spec/models/ci/build_trace_section_name_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceSectionName, model: true do
+ subject { build(:ci_build_trace_section_name) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:trace_sections)}
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+end
diff --git a/spec/models/ci/build_trace_section_spec.rb b/spec/models/ci/build_trace_section_spec.rb
new file mode 100644
index 00000000000..541a9a36fb8
--- /dev/null
+++ b/spec/models/ci/build_trace_section_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceSection, model: true do
+ it { is_expected.to belong_to(:build)}
+ it { is_expected.to belong_to(:project)}
+ it { is_expected.to belong_to(:section_name)}
+
+ it { is_expected.to validate_presence_of(:section_name) }
+ it { is_expected.to validate_presence_of(:build) }
+ it { is_expected.to validate_presence_of(:project) }
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 95da97b7bc5..2c9e7013b77 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -26,6 +26,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
describe '#source' do
context 'when creating new pipeline' do
@@ -237,7 +238,7 @@ describe Ci::Pipeline, :mailer do
describe '#stage_seeds' do
let(:pipeline) do
- create(:ci_pipeline, config: { rspec: { script: 'rake' } })
+ build(:ci_pipeline, config: { rspec: { script: 'rake' } })
end
it 'returns preseeded stage seeds object' do
@@ -246,6 +247,14 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#seeds_size' do
+ let(:pipeline) { build(:ci_pipeline_with_one_job) }
+
+ it 'returns number of jobs in stage seeds' do
+ expect(pipeline.seeds_size).to eq 1
+ end
+ end
+
describe '#legacy_stages' do
subject { pipeline.legacy_stages }
@@ -1439,4 +1448,24 @@ describe Ci::Pipeline, :mailer do
it_behaves_like 'not sending any notification'
end
end
+
+ describe '#latest_builds_with_artifacts' do
+ let!(:pipeline) { create(:ci_pipeline, :success) }
+
+ let!(:build) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline)
+ end
+
+ it 'returns the latest builds' do
+ expect(pipeline.latest_builds_with_artifacts).to eq([build])
+ end
+
+ it 'memoizes the returned relation' do
+ query_count = ActiveRecord::QueryRecorder
+ .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } }
+ .count
+
+ expect(query_count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb
index 2ce78e34b0c..889c243c8d8 100644
--- a/spec/models/ci/pipeline_variable_spec.rb
+++ b/spec/models/ci/pipeline_variable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::PipelineVariable, models: true do
+describe Ci::PipelineVariable do
subject { build(:ci_pipeline_variable) }
it { is_expected.to include_module(HasVariable) }
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 2e686e515c5..584dfe9a5c1 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -183,75 +183,42 @@ describe Ci::Runner do
end
end
- context 'when runner is locked' do
+ context 'when runner is shared' do
before do
- runner.locked = true
+ runner.is_shared = true
+ build.project.runners = []
end
- shared_examples 'locked build picker' do
- context 'when runner cannot pick untagged jobs' do
- before do
- runner.run_untagged = false
- end
+ it 'can handle builds' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
- it 'cannot handle builds without tags' do
- expect(runner.can_pick?(build)).to be_falsey
- end
+ context 'when runner is locked' do
+ before do
+ runner.locked = true
end
- context 'when having runner tags' do
- before do
- runner.tag_list = %w(bb cc)
- end
-
- it 'cannot handle it for builds without matching tags' do
- build.tag_list = ['aa']
-
- expect(runner.can_pick?(build)).to be_falsey
- end
+ it 'can handle builds' do
+ expect(runner.can_pick?(build)).to be_truthy
end
end
+ end
- context 'when serving the same project' do
- it 'can handle it' do
+ context 'when runner is not shared' do
+ context 'when runner is assigned to a project' do
+ it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
-
- it_behaves_like 'locked build picker'
-
- context 'when having runner tags' do
- before do
- runner.tag_list = %w(bb cc)
- build.tag_list = ['bb']
- end
-
- it 'can handle it for matching tags' do
- expect(runner.can_pick?(build)).to be_truthy
- end
- end
end
- context 'serving a different project' do
+ context 'when runner is not assigned to a project' do
before do
- runner.runner_projects.destroy_all
+ build.project.runners = []
end
- it 'cannot handle it' do
+ it 'cannot handle builds' do
expect(runner.can_pick?(build)).to be_falsey
end
-
- it_behaves_like 'locked build picker'
-
- context 'when having runner tags' do
- before do
- runner.tag_list = %w(bb cc)
- build.tag_list = ['bb']
- end
-
- it 'cannot handle it for matching tags' do
- expect(runner.can_pick?(build)).to be_falsey
- end
- end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 11e64a0f877..e3cfa149e3a 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -207,11 +207,6 @@ eos
context 'of a merge commit' do
let(:repository) { project.repository }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
-
let(:merge_request) do
create(:merge_request,
source_branch: 'video',
@@ -224,7 +219,7 @@ eos
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ 'Test message')
repository.commit(merge_commit_id)
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 40bbb10eaac..129dfa07f15 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -178,57 +178,59 @@ describe CacheMarkdownField do
end
end
- describe '#refresh_markdown_cache!' do
+ describe '#refresh_markdown_cache' do
before do
thing.foo = updated_markdown
end
- context 'do_update: false' do
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- it 'does not save the result' do
- expect(thing).not_to receive(:update_columns)
+ it 'does not save the result' do
+ expect(thing).not_to receive(:update_columns)
- thing.refresh_markdown_cache!
- end
+ thing.refresh_markdown_cache
+ end
- it 'updates the markdown cache version' do
- thing.cached_markdown_version = nil
- thing.refresh_markdown_cache!
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
- end
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
+ end
- context 'do_update: true' do
- it 'fills all html fields' do
- thing.refresh_markdown_cache!(do_update: true)
+ describe '#refresh_markdown_cache!' do
+ before do
+ thing.foo = updated_markdown
+ end
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
- it 'skips saving if not persisted' do
- expect(thing).to receive(:persisted?).and_return(false)
- expect(thing).not_to receive(:update_columns)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- thing.refresh_markdown_cache!(do_update: true)
- end
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
- it 'saves the changes using #update_columns' do
- expect(thing).to receive(:persisted?).and_return(true)
- expect(thing).to receive(:update_columns)
- .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+ thing.refresh_markdown_cache!
+ end
- thing.refresh_markdown_cache!(do_update: true)
- end
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+
+ thing.refresh_markdown_cache!
end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index a38f2553eb1..6866b43432c 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -231,6 +231,18 @@ describe HasStatus do
end
end
+ describe '.alive' do
+ subject { CommitStatus.alive }
+
+ %i[running pending created].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
describe '.created_or_pending' do
subject { CommitStatus.created_or_pending }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index b463d12e448..ab8773b7ede 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -12,6 +12,16 @@ describe Group, 'Routable' do
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
+ describe 'GitLab read-only instance' do
+ it 'does not save route if route is not present' do
+ group.route.path = ''
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(group).to receive(:update_route).and_call_original
+
+ expect { group.full_path }.to change { Route.count }.by(0)
+ end
+ end
+
describe 'Callbacks' do
it 'creates route record on create' do
expect(group.route.path).to eq(group.path)
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 4aa9ec789a3..eb0a3e9e0d3 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe DiffNote do
include RepoHelpers
- let(:merge_request) { create(:merge_request) }
+ let!(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:commit) { project.commit(sample_commit.id) }
@@ -98,14 +98,14 @@ describe DiffNote do
diff_line = subject.diff_line
expect(diff_line.added?).to be true
- expect(diff_line.new_line).to eq(position.new_line)
+ expect(diff_line.new_line).to eq(position.formatter.new_line)
expect(diff_line.text).to eq("+ vars = {")
end
end
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15)
+ line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15)
expect(subject.line_code).to eq(line_code)
end
@@ -255,4 +255,38 @@ describe DiffNote do
end
end
end
+
+ describe "image diff notes" do
+ let(:path) { "files/images/any_image.png" }
+
+ let!(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 10,
+ height: 10,
+ x: 1,
+ y: 1,
+ diff_refs: merge_request.diff_refs,
+ position_type: "image"
+ )
+ end
+
+ describe "validations" do
+ subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+
+ it { is_expected.not_to validate_presence_of(:line_code) }
+
+ it "does not validate diff line" do
+ diff_line = subject.diff_line
+
+ expect(diff_line).to be nil
+ expect(subject).to be_valid
+ end
+ end
+
+ it "returns true for on_image?" do
+ expect(subject.on_image?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 1d6fabe48b1..b32dd31ae6d 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -11,4 +11,33 @@ describe Email do
expect(described_class.new(email: ' inFO@exAMPLe.com ').email)
.to eq 'info@example.com'
end
+
+ describe '#update_invalid_gpg_signatures' do
+ let(:user) do
+ create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
+ user.skip_reconfirmation!
+ end
+ end
+ let(:user) { create(:user) }
+
+ it 'synchronizes the gpg keys when the email is updated' do
+ email = user.emails.create(email: 'new@email.com')
+
+ expect(user).to receive(:update_invalid_gpg_signatures)
+
+ email.confirm
+ end
+ end
+
+ describe 'scopes' do
+ let(:user) { create(:user) }
+
+ it 'scopes confirmed emails' do
+ create(:email, :confirmed, user: user)
+ create(:email, user: user)
+
+ expect(user.emails.count).to eq 2
+ expect(user.emails.confirmed.count).to eq 1
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index ea8512a5eae..25e5d155894 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -54,6 +54,28 @@ describe Environment do
end
end
+ describe '#folder_name' do
+ context 'when it is inside a folder' do
+ subject(:environment) do
+ create(:environment, name: 'staging/review-1')
+ end
+
+ it 'returns a top-level folder name' do
+ expect(environment.folder_name).to eq 'staging'
+ end
+ end
+
+ context 'when the environment if a top-level item itself' do
+ subject(:environment) do
+ create(:environment, name: 'production')
+ end
+
+ it 'returns an environment name' do
+ expect(environment.folder_name).to eq 'production'
+ end
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
new file mode 100644
index 00000000000..532ca1fca8c
--- /dev/null
+++ b/spec/models/fork_network_member_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+describe ForkNetworkMember do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:fork_network) }
+ end
+end
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
new file mode 100644
index 00000000000..605ccd6db06
--- /dev/null
+++ b/spec/models/fork_network_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe ForkNetwork do
+ include ProjectForksHelper
+
+ describe '#add_root_as_member' do
+ it 'adds the root project as a member when creating a new root network' do
+ project = create(:project)
+ fork_network = described_class.create(root_project: project)
+
+ expect(fork_network.projects).to include(project)
+ end
+ end
+
+ describe '#find_fork_in' do
+ it 'finds all fork of the current network in al collection' do
+ network = create(:fork_network)
+ root_project = network.root_project
+ another_project = fork_project(root_project)
+ create(:project)
+
+ expect(network.find_forks_in(Project.all))
+ .to contain_exactly(another_project, root_project)
+ end
+ end
+
+ context 'for a deleted project' do
+ it 'keeps the fork network' do
+ project = create(:project, :public)
+ forked = fork_project(project)
+ project.destroy!
+
+ fork_network = forked.reload.fork_network
+
+ expect(fork_network.projects).to contain_exactly(forked)
+ expect(fork_network.root_project).to be_nil
+ end
+
+ it 'allows multiple fork networks where the root project is deleted' do
+ first_project = create(:project)
+ second_project = create(:project)
+ first_fork = fork_project(first_project)
+ second_fork = fork_project(second_project)
+
+ first_project.destroy
+ second_project.destroy
+
+ expect(first_fork.fork_network).not_to be_nil
+ expect(first_fork.fork_network.root_project).to be_nil
+ expect(second_fork.fork_network).not_to be_nil
+ expect(second_fork.fork_network.root_project).to be_nil
+ end
+ end
+end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 7dbeb4d2e74..32e33e8f42f 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
+ include ProjectForksHelper
+
let(:project_from) { create(:project, :repository) }
let(:project_to) { fork_project(project_from, user) }
let(:user) { create(:user) }
- let(:namespace) { user.namespace }
before do
project_from.add_reporter(user)
@@ -64,13 +65,4 @@ describe ForkedProjectLink, "add link on fork" do
expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false)
end
end
-
- def fork_project(from_project, user)
- service = Projects::ForkService.new(from_project, user)
- shell = double('gitlab_shell', fork_repository: true)
-
- allow(service).to receive(:gitlab_shell).and_return(shell)
-
- service.execute
- end
end
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
new file mode 100644
index 00000000000..350fbc257d9
--- /dev/null
+++ b/spec/models/gcp/cluster_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe Gcp::Cluster do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:service) }
+
+ it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
+
+ describe '#default_value_for' do
+ let(:cluster) { described_class.new }
+
+ it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
+ it { expect(cluster.gcp_cluster_size).to eq(3) }
+ it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
+ end
+
+ describe '#validates' do
+ subject { cluster.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
+
+ context 'when valid' do
+ let(:gcp_project_id) { 'gcp-project-12345' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_project_id) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_name' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
+
+ context 'when valid' do
+ let(:gcp_cluster_name) { 'test-cluster' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_cluster_name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_cluster_name) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_cluster_name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_size' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
+
+ context 'when valid' do
+ let(:gcp_cluster_size) { 1 }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when zero' do
+ let(:gcp_cluster_size) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates project_namespace' do
+ let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
+
+ context 'when valid' do
+ let(:project_namespace) { 'default-namespace' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:project_namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when too long' do
+ let(:project_namespace) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:project_namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ before do
+ cluster.make_creating!
+ end
+
+ context 'when created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when creating' do
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ let(:cluster) { build(:gcp_cluster) }
+
+ context 'when transits to created state' do
+ before do
+ cluster.gcp_token = 'tmp'
+ cluster.gcp_operation_id = 'tmp'
+ cluster.make_created!
+ end
+
+ it 'nullify gcp_token and gcp_operation_id' do
+ expect(cluster.gcp_token).to be_nil
+ expect(cluster.gcp_operation_id).to be_nil
+ expect(cluster).to be_created
+ end
+ end
+
+ context 'when transits to errored state' do
+ let(:reason) { 'something wrong' }
+
+ before do
+ cluster.make_errored!(reason)
+ end
+
+ it 'sets status_reason' do
+ expect(cluster.status_reason).to eq(reason)
+ expect(cluster).to be_errored
+ end
+ end
+ end
+
+ describe '#project_namespace_placeholder' do
+ subject { cluster.project_namespace_placeholder }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'returns a placeholder' do
+ is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { cluster.on_creation? }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_url' do
+ subject { cluster.api_url }
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke) }
+ let(:api_url) { 'https://' + cluster.endpoint }
+
+ it { is_expected.to eq(api_url) }
+ end
+
+ describe '#restrict_modification' do
+ subject { cluster.restrict_modification }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'sets error' do
+ is_expected.to be_falsey
+ expect(cluster.errors).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index 9c99c3e5c08..33e6f1de3d1 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe GpgKey do
describe "associations" do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:subkeys) }
end
describe "validation" do
@@ -38,6 +39,14 @@ describe GpgKey do
expect(gpg_key.primary_keyid).to eq GpgHelpers::User1.primary_keyid
end
end
+
+ describe 'generate_subkeys' do
+ it 'extracts the subkeys from the gpg key' do
+ gpg_key = create(:gpg_key, key: GpgHelpers::User1.public_key_with_extra_signing_key)
+
+ expect(gpg_key.subkeys.count).to eq(2)
+ end
+ end
end
describe '#key=' do
@@ -90,11 +99,20 @@ describe GpgKey do
it 'email is verified if the user has the matching email' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
+ create :email, user: user
+ user.reload
expect(gpg_key.emails_with_verified_status).to eq(
'bette.cartwright@example.com' => true,
'bette.cartwright@example.net' => false
)
+
+ create :email, :confirmed, user: user, email: 'bette.cartwright@example.net'
+ user.reload
+ expect(gpg_key.emails_with_verified_status).to eq(
+ 'bette.cartwright@example.com' => true,
+ 'bette.cartwright@example.net' => true
+ )
end
end
@@ -138,17 +156,13 @@ describe GpgKey do
expect(gpg_key.verified?).to be_truthy
expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.com')).to be_truthy
end
- end
- describe 'notification', :mailer do
- let(:user) { create(:user) }
-
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:gpg_key, user: user)
- end
+ it 'returns true if one of the email addresses in the key belongs to the user and case-insensitively matches the provided email' do
+ user = create :user, email: 'bette.cartwright@example.com'
+ gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
- should_email(user)
+ expect(gpg_key.verified?).to be_truthy
+ expect(gpg_key.verified_and_belongs_to_email?('Bette.Cartwright@example.com')).to be_truthy
end
end
@@ -177,5 +191,29 @@ describe GpgKey do
expect(unrelated_gpg_key.destroyed?).to be false
end
+
+ it 'deletes all the associated subkeys' do
+ gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
+
+ expect(gpg_key.subkeys).to be_present
+
+ gpg_key.revoke
+
+ expect(gpg_key.subkeys(true)).to be_blank
+ end
+
+ it 'invalidates all signatures associated to the subkeys' do
+ gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
+ gpg_key_subkey = gpg_key.subkeys.last
+ gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key_subkey
+
+ gpg_key.revoke
+
+ expect(gpg_signature.reload).to have_attributes(
+ verification_status: 'unknown_key',
+ gpg_key: nil,
+ gpg_key_subkey: nil
+ )
+ end
end
end
diff --git a/spec/models/gpg_key_subkey_spec.rb b/spec/models/gpg_key_subkey_spec.rb
new file mode 100644
index 00000000000..3c86837f47f
--- /dev/null
+++ b/spec/models/gpg_key_subkey_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe GpgKeySubkey do
+ subject { build(:gpg_key_subkey) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:gpg_key) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:gpg_key_id) }
+ it { is_expected.to validate_presence_of(:fingerprint) }
+ it { is_expected.to validate_presence_of(:keyid) }
+ end
+end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index c58fd46762a..0136bb61c07 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -1,9 +1,17 @@
require 'rails_helper'
RSpec.describe GpgSignature do
+ let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let!(:project) { create(:project, :repository, path: 'sample-project') }
+ let!(:commit) { create(:commit, project: project, sha: commit_sha) }
+ let(:gpg_signature) { create(:gpg_signature, commit_sha: commit_sha) }
+ let(:gpg_key) { create(:gpg_key) }
+ let(:gpg_key_subkey) { create(:gpg_key_subkey) }
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:gpg_key) }
+ it { is_expected.to belong_to(:gpg_key_subkey) }
end
describe 'validation' do
@@ -15,14 +23,48 @@ RSpec.describe GpgSignature do
describe '#commit' do
it 'fetches the commit through the project' do
- commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
- project = create :project, :repository
- commit = create :commit, project: project
- gpg_signature = create :gpg_signature, commit_sha: commit_sha
-
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
gpg_signature.commit
end
end
+
+ describe '#gpg_key=' do
+ it 'supports the assignment of a GpgKey' do
+ gpg_signature = create(:gpg_signature, gpg_key: gpg_key)
+
+ expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKey)
+ end
+
+ it 'supports the assignment of a GpgKeySubkey' do
+ gpg_signature = create(:gpg_signature, gpg_key: gpg_key_subkey)
+
+ expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKeySubkey)
+ end
+
+ it 'clears gpg_key and gpg_key_subkey_id when passing nil' do
+ gpg_signature.update_attribute(:gpg_key, nil)
+
+ expect(gpg_signature.gpg_key_id).to be_nil
+ expect(gpg_signature.gpg_key_subkey_id).to be_nil
+ end
+ end
+
+ describe '#gpg_commit' do
+ context 'when commit does not exist' do
+ it 'returns nil' do
+ allow(gpg_signature).to receive(:commit).and_return(nil)
+
+ expect(gpg_signature.gpg_commit).to be_nil
+ end
+ end
+
+ context 'when commit exists' do
+ it 'returns an instance of Gitlab::Gpg::Commit' do
+ allow(gpg_signature).to receive(:commit).and_return(commit)
+
+ expect(gpg_signature.gpg_commit).to be_an_instance_of(Gitlab::Gpg::Commit)
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 96baeaff0a4..81c2057e175 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -37,30 +37,17 @@ describe Key, :mailer do
end
describe "#update_last_used_at" do
- let(:key) { create(:key) }
-
- context 'when key was not updated during the last day' do
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return('000000')
- end
-
- it 'enqueues a UseKeyWorker job' do
- expect(UseKeyWorker).to receive(:perform_async).with(key.id)
- key.update_last_used_at
- end
- end
+ it 'updates the last used timestamp' do
+ key = build(:key)
+ service = double(:service)
+
+ expect(Keys::LastUsedService).to receive(:new)
+ .with(key)
+ .and_return(service)
- context 'when key was updated during the last day' do
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(false)
- end
+ expect(service).to receive(:execute)
- it 'does not enqueue a UseKeyWorker job' do
- expect(UseKeyWorker).not_to receive(:perform_async)
- key.update_last_used_at
- end
+ key.update_last_used_at
end
end
end
@@ -168,17 +155,15 @@ describe Key, :mailer do
it 'strips white spaces' do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
- end
- describe 'notification' do
- let(:user) { create(:user) }
+ it 'invalidates the public_key attribute' do
+ key = build(:key)
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:key, user: user)
- end
+ original = key.public_key
+ key.key = valid_key
- should_email(user)
+ expect(original.key_text).not_to be_nil
+ expect(key.public_key.key_text).to eq(valid_key)
end
end
end
diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb
index d24d4cf7695..0a3180f43e8 100644
--- a/spec/models/lfs_objects_project_spec.rb
+++ b/spec/models/lfs_objects_project_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
describe LfsObjectsProject do
- subject { create(:lfs_objects_project, project: project) }
- let(:project) { create(:project) }
+ set(:project) { create(:project) }
+
+ subject do
+ create(:lfs_objects_project, project: project)
+ end
describe 'associations' do
it { is_expected.to belong_to(:project) }
@@ -11,9 +14,13 @@ describe LfsObjectsProject do
describe 'validation' do
it { is_expected.to validate_presence_of(:lfs_object_id) }
- it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") }
-
it { is_expected.to validate_presence_of(:project_id) }
+
+ it 'validates object id' do
+ is_expected.to validate_uniqueness_of(:lfs_object_id)
+ .scoped_to(:project_id)
+ .with_message("already exists in project")
+ end
end
describe '#update_project_statistics' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index a07ce05a865..0a017c068ad 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -488,7 +488,7 @@ describe Member do
member.accept_invite!(user)
end
- it "refreshes user's authorized projects", truncate: true do
+ it "refreshes user's authorized projects", :truncate do
project = member.source
expect(user.authorized_projects).not_to include(project)
@@ -523,7 +523,7 @@ describe Member do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d80d5657c42..950af653c80 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe MergeRequest do
include RepoHelpers
+ include ProjectForksHelper
subject { create(:merge_request) }
@@ -49,6 +50,19 @@ describe MergeRequest do
expect(subject).to be_valid
end
end
+
+ context 'for forks' do
+ let(:project) { create(:project) }
+ let(:fork1) { fork_project(project) }
+ let(:fork2) { fork_project(project) }
+
+ it 'allows merge requests for sibling-forks' do
+ subject.source_project = fork1
+ subject.target_project = fork2
+
+ expect(subject).to be_valid
+ end
+ end
end
describe 'respond to' do
@@ -672,7 +686,7 @@ describe MergeRequest do
describe '#diverged_commits_count' do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
context 'when the target branch does not exist anymore' do
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -700,7 +714,7 @@ describe MergeRequest do
end
context 'diverged on fork' do
- subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
+ subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(29)
@@ -708,7 +722,7 @@ describe MergeRequest do
end
context 'rebased on fork' do
- subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) }
+ subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(0)
@@ -791,6 +805,49 @@ describe MergeRequest do
end
end
+ describe '#has_ci?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ context 'has ci' do
+ it 'returns true if MR has head_pipeline_id and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { double }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+
+ it 'returns true if MR has any pipeline and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+ allow(merge_request).to receive(:all_pipelines) { [double] }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+
+ it 'returns true if MR has CI service and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+ allow(merge_request).to receive(:all_pipelines) { [] }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+ end
+
+ context 'has no ci' do
+ it 'returns false if MR has no CI service nor pipeline, and no commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:all_pipelines) { [] }
+ allow(merge_request).to receive(:has_no_commits?) { true }
+
+ expect(merge_request.has_ci?).to be(false)
+ end
+ end
+ end
+
describe '#all_pipelines' do
shared_examples 'returning pipelines with proper ordering' do
let!(:all_pipelines) do
@@ -1214,11 +1271,7 @@ describe MergeRequest do
end
context 'with environments on source project' do
- let(:source_project) do
- create(:project, :repository) do |fork_project|
- fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- end
- end
+ let(:source_project) { fork_project(project, nil, repository: true) }
let(:merge_request) do
create(:merge_request,
@@ -1382,14 +1435,14 @@ describe MergeRequest do
describe "#source_project_missing?" do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let(:user) { create(:user) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
context "when the fork exists" do
let(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1403,9 +1456,9 @@ describe MergeRequest do
end
context "when the fork does not exist" do
- let(:merge_request) do
+ let!(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1428,14 +1481,14 @@ describe MergeRequest do
describe "#closed_without_fork?" do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let(:user) { create(:user) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
context "when the merge request is closed" do
let(:closed_merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1454,7 +1507,7 @@ describe MergeRequest do
context "when the merge request is open" do
let(:open_merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1473,24 +1526,24 @@ describe MergeRequest do
end
context 'forked project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:user) { create(:user) }
- let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:forked_project) { fork_project(project, user) }
let!(:merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
it 'returns false if unforked' do
- Projects::UnlinkForkService.new(fork_project, user).execute
+ Projects::UnlinkForkService.new(forked_project, user).execute
expect(merge_request.reload.reopenable?).to be_falsey
end
it 'returns false if the source project is deleted' do
- Projects::DestroyService.new(fork_project, user).execute
+ Projects::DestroyService.new(forked_project, user).execute
expect(merge_request.reload.reopenable?).to be_falsey
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d3da0107d5c..13e37fffa4e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -238,7 +238,7 @@ describe Milestone do
let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
it 'returns a String reference to the object' do
- expect(milestone.to_reference).to eq '%1'
+ expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
@@ -246,24 +246,29 @@ describe Milestone do
end
it 'supports a cross-project reference' do
- expect(milestone.to_reference(another_project)).to eq 'sample-project%1'
+ expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
end
end
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
- it 'returns nil with the default format' do
- expect(milestone.to_reference).to be_nil
+ it 'returns a group milestone reference with a default format' do
+ expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
end
- it 'does not supports cross-project references' do
+ it 'does supports cross-project references within a group' do
expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
end
+
+ it 'raises an error when using iid format' do
+ expect { milestone.to_reference(format: :iid) }
+ .to raise_error(ArgumentError, 'Cannot refer to a group milestone by an internal id!')
+ end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 81d5ab7a6d3..2ebf6acd42a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Namespace do
+ include ProjectForksHelper
+
let!(:namespace) { create(:namespace) }
describe 'associations' do
@@ -152,22 +154,24 @@ describe Namespace do
end
describe '#move_dir' do
+ let(:namespace) { create(:namespace) }
+ let!(:project) { create(:project_empty_repo, namespace: namespace) }
+
before do
- @namespace = create :namespace
- @project = create(:project_empty_repo, namespace: @namespace)
- allow(@namespace).to receive(:path_changed?).and_return(true)
+ allow(namespace).to receive(:path_changed?).and_return(true)
end
it "raises error when directory exists" do
- expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
+ expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end
it "moves dir if path changed" do
- new_path = @namespace.full_path + "_new"
- allow(@namespace).to receive(:full_path_was).and_return(@namespace.full_path)
- allow(@namespace).to receive(:full_path).and_return(new_path)
- expect(@namespace).to receive(:remove_exports!)
- expect(@namespace.move_dir).to be_truthy
+ new_path = namespace.full_path + "_new"
+
+ allow(namespace).to receive(:full_path_was).and_return(namespace.full_path)
+ allow(namespace).to receive(:full_path).and_return(new_path)
+ expect(namespace).to receive(:remove_exports!)
+ expect(namespace.move_dir).to be_truthy
end
context "when any project has container images" do
@@ -177,14 +181,14 @@ describe Namespace do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: ['tag'])
- create(:project, namespace: @namespace, container_repositories: [container_repository])
+ create(:project, namespace: namespace, container_repositories: [container_repository])
- allow(@namespace).to receive(:path_was).and_return(@namespace.path)
- allow(@namespace).to receive(:path).and_return('new_path')
+ allow(namespace).to receive(:path_was).and_return(namespace.path)
+ allow(namespace).to receive(:path).and_return('new_path')
end
it 'raises an error about not movable project' do
- expect { @namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
+ expect { namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
end
end
@@ -518,4 +522,25 @@ describe Namespace do
end
end
end
+
+ describe '#has_forks_of?' do
+ let(:project) { create(:project, :public) }
+ let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
+
+ before do
+ # Reset the fork network relation
+ project.reload
+ end
+
+ it 'knows if there is a direct fork in the namespace' do
+ expect(namespace.find_fork_of(project)).to eq(forked_project)
+ end
+
+ it 'knows when there is as fork-of-fork in the namespace' do
+ other_namespace = create(:namespace)
+ other_fork = fork_project(forked_project, other_namespace.owner, namespace: other_namespace)
+
+ expect(other_namespace.find_fork_of(project)).to eq(other_fork)
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index b214074fdce..1ecb50586c7 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -314,6 +314,56 @@ describe Note do
expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
end
+
+ context 'with image discussions' do
+ let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images and changes") }
+ let(:image_path) { "files/images/ee_repo_logo.png" }
+ let(:text_path) { "bar/branch-test.txt" }
+ let!(:image_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) }
+ let!(:text_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: text_position) }
+
+ let(:image_position) do
+ Gitlab::Diff::Position.new(
+ old_path: image_path,
+ new_path: image_path,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 1,
+ position_type: "image",
+ diff_refs: merge_request2.diff_refs
+ )
+ end
+
+ let(:text_position) do
+ Gitlab::Diff::Position.new(
+ old_path: text_path,
+ new_path: text_path,
+ old_line: nil,
+ new_line: 2,
+ position_type: "text",
+ diff_refs: merge_request2.diff_refs
+ )
+ end
+
+ it "groups image discussions by file identifier" do
+ diff_discussion = DiffDiscussion.new([image_note])
+
+ discussions = merge_request2.notes.grouped_diff_discussions
+
+ expect(discussions.size).to eq(2)
+ expect(discussions[image_note.diff_file.new_path]).to include(diff_discussion)
+ end
+
+ it "groups text discussions by line code" do
+ diff_discussion = DiffDiscussion.new([text_note])
+
+ discussions = merge_request2.notes.grouped_diff_discussions
+
+ expect(discussions.size).to eq(2)
+ expect(discussions[text_note.line_code]).to include(diff_discussion)
+ end
+ end
end
context 'diff discussions for older diff refs' do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index b2f2a3ce914..01440b15674 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -41,7 +41,7 @@ describe PersonalAccessToken do
it 'revokes the token' do
active_personal_access_token.revoke!
- expect(active_personal_access_token.revoked?).to be true
+ expect(active_personal_access_token).to be_revoked
end
end
@@ -61,10 +61,37 @@ describe PersonalAccessToken do
expect(personal_access_token).to be_valid
end
- it "allows creating a token with read_registry scope" do
- personal_access_token.scopes = [:read_registry]
+ context 'when registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
- expect(personal_access_token).to be_valid
+ it "rejects creating a token with read_registry scope" do
+ personal_access_token.scopes = [:read_registry]
+
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes"
+ end
+
+ it "allows revoking a token with read_registry scope" do
+ personal_access_token.scopes = [:read_registry]
+
+ personal_access_token.revoke!
+
+ expect(personal_access_token).to be_revoked
+ end
+ end
+
+ context 'when registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ it "allows creating a token with read_registry scope" do
+ personal_access_token.scopes = [:read_registry]
+
+ expect(personal_access_token).to be_valid
+ end
end
it "rejects creating a token with unavailable scopes" do
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index ca13af4d73e..12069575866 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -8,7 +8,21 @@ describe ProjectAutoDevops do
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
- describe 'variables' do
+ describe '#has_domain?' do
+ context 'when domain is defined' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: 'domain.com') }
+
+ it { expect(auto_devops).to have_domain }
+ end
+
+ context 'when domain is empty' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: '') }
+
+ it { expect(auto_devops).not_to have_domain }
+ end
+ end
+
+ describe '#variables' do
let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) }
context 'when domain is defined' do
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index b3513c80150..41e2ab20d69 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -30,7 +30,7 @@ describe ProjectGroupLink do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes group users' authorized projects" do
project = create(:project, :private)
group = create(:group)
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 4bb1db684e6..d37726dc3f1 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -42,7 +42,7 @@ describe ChatMessage::IssueMessage do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '[<http://somewhere.com|project_name>] Issue opened by test.user')
+ '[<http://somewhere.com|project_name>] Issue opened by Test User (test.user)')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
@@ -62,7 +62,7 @@ describe ChatMessage::IssueMessage do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user')
+ '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by Test User (test.user)')
expect(subject.attachments).to be_empty
end
end
@@ -76,10 +76,10 @@ describe ChatMessage::IssueMessage do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '[[project_name](http://somewhere.com)] Issue opened by test.user')
+ '[[project_name](http://somewhere.com)] Issue opened by Test User (test.user)')
expect(subject.attachments).to eq('issue description')
expect(subject.activity).to eq({
- title: 'Issue opened by test.user',
+ title: 'Issue opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
@@ -95,10 +95,10 @@ describe ChatMessage::IssueMessage do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by test.user')
+ '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by Test User (test.user)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Issue closed by test.user',
+ title: 'Issue closed by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index b600a36f578..184a07ae0f9 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -33,7 +33,7 @@ describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'test.user opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ 'Test User (test.user) opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
@@ -44,7 +44,7 @@ describe ChatMessage::MergeMessage do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'test.user closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ 'Test User (test.user) closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
@@ -58,10 +58,10 @@ describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'test.user opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request opened by test.user',
+ title: 'Merge Request opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
@@ -76,10 +76,10 @@ describe ChatMessage::MergeMessage do
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'test.user closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request closed by test.user',
+ title: 'Merge Request closed by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index a09c2f9935c..5abbd7bec18 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -38,7 +38,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on commits' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"commit 5f163b2b> in <http://somewhere.com|project_name>: " \
"*Added a commit message*")
expect(subject.attachments).to eq([{
@@ -55,11 +55,11 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on commits' do
expect(subject.pretext).to eq(
- 'test.user [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
+ 'Test User (test.user) [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
)
expect(subject.attachments).to eq('comment on a commit')
expect(subject.activity).to eq({
- title: 'test.user [commented on commit 5f163b2b](http://url.com)',
+ title: 'Test User (test.user) [commented on commit 5f163b2b](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Added a commit message',
image: 'http://fakeavatar'
@@ -81,7 +81,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on a merge request' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"merge request !30> in <http://somewhere.com|project_name>: " \
"*merge request title*")
expect(subject.attachments).to eq([{
@@ -98,10 +98,10 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on a merge request' do
expect(subject.pretext).to eq(
- 'test.user [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
+ 'Test User (test.user) [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
expect(subject.attachments).to eq('comment on a merge request')
expect(subject.activity).to eq({
- title: 'test.user [commented on merge request !30](http://url.com)',
+ title: 'Test User (test.user) [commented on merge request !30](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'merge request title',
image: 'http://fakeavatar'
@@ -124,7 +124,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on an issue' do
expect(subject.pretext).to eq(
- "test.user <http://url.com|commented on " \
+ "Test User (test.user) <http://url.com|commented on " \
"issue #20> in <http://somewhere.com|project_name>: " \
"*issue title*")
expect(subject.attachments).to eq([{
@@ -141,10 +141,10 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on an issue' do
expect(subject.pretext).to eq(
- 'test.user [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
+ 'Test User (test.user) [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
expect(subject.attachments).to eq('comment on an issue')
expect(subject.activity).to eq({
- title: 'test.user [commented on issue #20](http://url.com)',
+ title: 'Test User (test.user) [commented on issue #20](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'issue title',
image: 'http://fakeavatar'
@@ -165,7 +165,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on a project snippet' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"snippet $5> in <http://somewhere.com|project_name>: " \
"*snippet title*")
expect(subject.attachments).to eq([{
@@ -182,7 +182,7 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on a project snippet' do
expect(subject.pretext).to eq(
- 'test.user [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
+ 'Test User (test.user) [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
expect(subject.attachments).to eq('comment on a snippet')
end
end
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index 43b02568cb9..0ff20400999 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
- let(:user) { { name: 'hacker' } }
+ let(:user) { { name: "The Hacker", username: 'hacker' } }
let(:duration) { 7210 }
let(:args) do
{
@@ -22,12 +22,13 @@ describe ChatMessage::PipelineMessage do
user: user
}
end
+ let(:combined_name) { "The Hacker (hacker)" }
context 'without markdown' do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:message) { build_message('passed') }
+ let(:message) { build_message('passed', combined_name) }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
@@ -39,7 +40,7 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:message) { build_message }
+ let(:message) { build_message(status, combined_name) }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
@@ -75,13 +76,13 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:message) { build_markdown_message('passed') }
+ let(:message) { build_markdown_message('passed', combined_name) }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
@@ -92,13 +93,13 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:message) { build_markdown_message }
+ let(:message) { build_markdown_message(status, combined_name) }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index c4adee4f489..7efcba9bcfd 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -29,7 +29,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ 'Test User (test.user) created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -41,7 +41,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ 'Test User (test.user) edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -95,7 +95,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'test.user created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ 'Test User (test.user) created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
@@ -106,7 +106,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'test.user edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ 'Test User (test.user) edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
end
@@ -141,7 +141,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns the attachment for a new wiki page' do
expect(subject.activity).to eq({
- title: 'test.user created [wiki page](http://url.com)',
+ title: 'Test User (test.user) created [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
@@ -156,7 +156,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns the attachment for an updated wiki page' do
expect(subject.activity).to eq({
- title: 'test.user edited [wiki page](http://url.com)',
+ title: 'Test User (test.user) edited [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 537cdadd528..2298dcab55f 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -208,7 +208,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
config.dig('users', 0, 'user')['token'] = 'token'
config.dig('contexts', 0, 'context')['namespace'] = namespace
config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
- Base64.encode64('CA PEM DATA')
+ Base64.strict_encode64('CA PEM DATA')
YAML.dump(config)
end
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 5faab9ba38b..be07ca2d945 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -6,7 +6,8 @@ describe PipelinesEmailService, :mailer do
end
let(:project) { create(:project, :repository) }
- let(:recipient) { 'test@gitlab.com' }
+ let(:recipients) { 'test@gitlab.com' }
+ let(:receivers) { [recipients] }
let(:data) do
Gitlab::DataBuilder::Pipeline.build(pipeline)
@@ -48,18 +49,24 @@ describe PipelinesEmailService, :mailer do
shared_examples 'sending email' do
before do
+ subject.recipients = recipients
+
perform_enqueued_jobs do
run
end
end
it 'sends email' do
- should_only_email(double(notification_email: recipient), kind: :bcc)
+ emails = receivers.map { |r| double(notification_email: r) }
+
+ should_only_email(*emails, kind: :bcc)
end
end
shared_examples 'not sending email' do
before do
+ subject.recipients = recipients
+
perform_enqueued_jobs do
run
end
@@ -75,10 +82,6 @@ describe PipelinesEmailService, :mailer do
subject.test(data)
end
- before do
- subject.recipients = recipient
- end
-
context 'when pipeline is failed' do
before do
data[:object_attributes][:status] = 'failed'
@@ -104,10 +107,6 @@ describe PipelinesEmailService, :mailer do
end
context 'with recipients' do
- before do
- subject.recipients = recipient
- end
-
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
@@ -152,9 +151,7 @@ describe PipelinesEmailService, :mailer do
end
context 'with empty recipients list' do
- before do
- subject.recipients = ' ,, '
- end
+ let(:recipients) { ' ,, ' }
context 'with failed pipeline' do
before do
@@ -165,5 +162,19 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'not sending email'
end
end
+
+ context 'with recipients list separating with newlines' do
+ let(:recipients) { "\ntest@gitlab.com, \r\nexample@gitlab.com" }
+ let(:receivers) { %w[test@gitlab.com example@gitlab.com] }
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 48fc77423ff..a26c71e5155 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -57,6 +57,7 @@ describe Project do
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
+ it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:active_runners) }
@@ -76,6 +77,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_one(:cluster) }
context 'after initialized' do
it "has a project_feature" do
@@ -408,21 +410,23 @@ describe Project do
end
end
- describe '#repository_storage_path' do
- let(:project) { create(:project, repository_storage: 'custom') }
-
- before do
- FileUtils.mkdir('tmp/tests/custom_repositories')
- storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ describe '#merge_method' do
+ it 'returns "ff" merge_method when ff is enabled' do
+ project = build(:project, merge_requests_ff_only_enabled: true)
+ expect(project.merge_method).to be :ff
end
- after do
- FileUtils.rm_rf('tmp/tests/custom_repositories')
+ it 'returns "merge" merge_method when ff is disabled' do
+ project = build(:project, merge_requests_ff_only_enabled: false)
+ expect(project.merge_method).to be :merge
end
+ end
+
+ describe '#repository_storage_path' do
+ let(:project) { create(:project) }
it 'returns the repository storage path' do
- expect(project.repository_storage_path).to eq('tmp/tests/custom_repositories')
+ expect(Dir.exist?(project.repository_storage_path)).to be(true)
end
end
@@ -689,6 +693,44 @@ describe Project do
project.cache_has_external_issue_tracker
end.to change { project.has_external_issue_tracker}.to(false)
end
+
+ it 'does not cache data when in a read-only GitLab instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect do
+ project.cache_has_external_issue_tracker
+ end.not_to change { project.has_external_issue_tracker }
+ end
+ end
+
+ describe '#cache_has_external_wiki' do
+ let(:project) { create(:project, has_external_wiki: nil) }
+
+ it 'stores true if there is any external_wikis' do
+ services = double(:service, external_wikis: [ExternalWikiService.new])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_wiki
+ end.to change { project.has_external_wiki}.to(true)
+ end
+
+ it 'stores false if there is no external_wikis' do
+ services = double(:service, external_wikis: [])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_wiki
+ end.to change { project.has_external_wiki}.to(false)
+ end
+
+ it 'does not cache data when in a read-only GitLab instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect do
+ project.cache_has_external_wiki
+ end.not_to change { project.has_external_wiki }
+ end
end
describe '#has_wiki?' do
@@ -1303,7 +1345,7 @@ describe Project do
context 'using a regular repository' do
it 'creates the repository' do
expect(shell).to receive(:add_repository)
- .with(project.repository_storage_path, project.disk_path)
+ .with(project.repository_storage, project.disk_path)
.and_return(true)
expect(project.repository).to receive(:after_create)
@@ -1313,7 +1355,7 @@ describe Project do
it 'adds an error if the repository could not be created' do
expect(shell).to receive(:add_repository)
- .with(project.repository_storage_path, project.disk_path)
+ .with(project.repository_storage, project.disk_path)
.and_return(false)
expect(project.repository).not_to receive(:after_create)
@@ -1370,7 +1412,7 @@ describe Project do
.and_return(false)
expect(shell).to receive(:add_repository)
- .with(project.repository_storage_path, project.disk_path)
+ .with(project.repository_storage, project.disk_path)
.and_return(true)
project.ensure_repository
@@ -1814,6 +1856,59 @@ describe Project do
end
end
+ context 'forks' do
+ include ProjectForksHelper
+
+ let(:project) { create(:project, :public) }
+ let!(:forked_project) { fork_project(project) }
+
+ describe '#fork_network' do
+ it 'includes a fork of the project' do
+ expect(project.fork_network.projects).to include(forked_project)
+ end
+
+ it 'includes a fork of a fork' do
+ other_fork = fork_project(forked_project)
+
+ expect(project.fork_network.projects).to include(other_fork)
+ end
+
+ it 'includes sibling forks' do
+ other_fork = fork_project(project)
+
+ expect(forked_project.fork_network.projects).to include(other_fork)
+ end
+
+ it 'includes the base project' do
+ expect(forked_project.fork_network.projects).to include(project.reload)
+ end
+ end
+
+ describe '#in_fork_network_of?' do
+ it 'is true for a real fork' do
+ expect(forked_project.in_fork_network_of?(project)).to be_truthy
+ end
+
+ it 'is true for a fork of a fork', :postgresql do
+ other_fork = fork_project(forked_project)
+
+ expect(other_fork.in_fork_network_of?(project)).to be_truthy
+ end
+
+ it 'is true for sibling forks' do
+ sibling = fork_project(project)
+
+ expect(sibling.in_fork_network_of?(forked_project)).to be_truthy
+ end
+
+ it 'is false when another project is given' do
+ other_project = build_stubbed(:project)
+
+ expect(forked_project.in_fork_network_of?(other_project)).to be_falsy
+ end
+ end
+ end
+
describe '#pushes_since_gc' do
let(:project) { create(:project) }
@@ -2363,12 +2458,24 @@ describe Project do
describe '#legacy_storage?' do
it 'returns true when storage_version is nil' do
- project = build(:project)
+ project = build(:project, storage_version: nil)
+
+ expect(project.legacy_storage?).to be_truthy
+ end
+
+ it 'returns true when the storage_version is 0' do
+ project = build(:project, storage_version: 0)
expect(project.legacy_storage?).to be_truthy
end
end
+ describe '#hashed_storage?' do
+ it 'returns false' do
+ expect(project.hashed_storage?).to be_falsey
+ end
+ end
+
describe '#rename_repo' do
before do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
@@ -2425,6 +2532,38 @@ describe Project do
expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path))
end
end
+
+ describe '#migrate_to_hashed_storage!' do
+ it 'returns true' do
+ expect(project.migrate_to_hashed_storage!).to be_truthy
+ end
+
+ it 'flags as read-only' do
+ expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true)
+ end
+
+ it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the project repo is in use' do
+ Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: false)).increase
+
+ expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in)
+
+ project.migrate_to_hashed_storage!
+ end
+
+ it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the wiki repo is in use' do
+ Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: true)).increase
+
+ expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in)
+
+ project.migrate_to_hashed_storage!
+ end
+
+ it 'schedules ProjectMigrateHashedStorageWorker' do
+ expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).with(project.id)
+
+ project.migrate_to_hashed_storage!
+ end
+ end
end
context 'hashed storage' do
@@ -2438,6 +2577,18 @@ describe Project do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
end
+ describe '#legacy_storage?' do
+ it 'returns false' do
+ expect(project.legacy_storage?).to be_falsey
+ end
+ end
+
+ describe '#hashed_storage?' do
+ it 'returns true' do
+ expect(project.hashed_storage?).to be_truthy
+ end
+ end
+
describe '#base_dir' do
it 'returns base_dir based on hash of project id' do
expect(project.base_dir).to eq('@hashed/6b/86')
@@ -2508,6 +2659,26 @@ describe Project do
expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path))
end
end
+
+ describe '#migrate_to_hashed_storage!' do
+ it 'returns nil' do
+ expect(project.migrate_to_hashed_storage!).to be_nil
+ end
+
+ it 'does not flag as read-only' do
+ expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only }
+ end
+ end
+ end
+
+ describe '#gl_repository' do
+ let(:project) { create(:project) }
+
+ it 'delegates to Gitlab::GlRepository.gl_repository' do
+ expect(Gitlab::GlRepository).to receive(:gl_repository).with(project, true)
+
+ project.gl_repository(is_wiki: true)
+ end
end
describe '#has_ci?' do
@@ -2682,4 +2853,71 @@ describe Project do
end
end
end
+
+ describe '#latest_successful_builds_for' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ context 'without a ref' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for
+ end
+ end
+
+ context 'with the ref set to the default branch' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for(project.default_branch)
+ end
+ end
+
+ context 'with a ref that is not the default branch' do
+ it 'returns the latest successful pipeline for the given ref' do
+ expect(project.pipelines).to receive(:latest_successful_for).with('foo')
+
+ project.latest_successful_pipeline_for('foo')
+ end
+ end
+ end
+
+ describe '#check_repository_path_availability' do
+ let(:project) { build(:project) }
+
+ it 'skips gitlab-shell exists?' do
+ project.skip_disk_validation = true
+
+ expect(project.gitlab_shell).not_to receive(:exists?)
+ expect(project.check_repository_path_availability).to be_truthy
+ end
+ end
+
+ describe '#latest_successful_pipeline_for_default_branch' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ it 'memoizes and returns the latest successful pipeline for the default branch' do
+ pipeline = double(:pipeline)
+
+ expect(project.pipelines).to receive(:latest_successful_for)
+ .with(project.default_branch)
+ .and_return(pipeline)
+ .once
+
+ 2.times do
+ expect(project.latest_successful_pipeline_for_default_branch)
+ .to eq(pipeline)
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 953df7746eb..78fb2df884a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -6,13 +6,10 @@ describe ProjectWiki do
let(:user) { project.owner }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project_wiki) { described_class.new(project, user) }
+ let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') }
subject { project_wiki }
- before do
- project_wiki.wiki
- end
-
describe "#path_with_namespace" do
it "returns the project path with namespace with the .wiki extension" do
expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
@@ -61,8 +58,8 @@ describe ProjectWiki do
end
describe "#wiki" do
- it "contains a Gollum::Wiki instance" do
- expect(subject.wiki).to be_a Gollum::Wiki
+ it "contains a Gitlab::Git::Wiki instance" do
+ expect(subject.wiki).to be_a Gitlab::Git::Wiki
end
it "creates a new wiki repo if one does not yet exist" do
@@ -70,20 +67,18 @@ describe ProjectWiki do
end
it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
- allow(project_wiki).to receive(:init_repo).and_return(false)
- expect { project_wiki.send(:create_repo!) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
+ # Create a fresh project which will not have a wiki
+ project_wiki = described_class.new(create(:project), user)
+ gitlab_shell = double(:gitlab_shell)
+ allow(gitlab_shell).to receive(:add_repository)
+ allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell)
+
+ expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
end
end
describe "#empty?" do
context "when the wiki repository is empty" do
- before do
- allow_any_instance_of(Gitlab::Shell).to receive(:add_repository) do
- create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git")
- end
- allow(project).to receive(:full_path).and_return("non-existant")
- end
-
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_truthy }
@@ -154,13 +149,13 @@ describe ProjectWiki do
before do
file = Gollum::File.new(subject.wiki)
allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('image.jpg', 'master', true)
+ .to receive(:file).with('image.jpg', 'master')
.and_return(file)
allow_any_instance_of(Gollum::File)
.to receive(:mime_type)
.and_return('image/jpeg')
allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('non-existant', 'master', true)
+ .to receive(:file).with('non-existant', 'master')
.and_return(nil)
end
@@ -178,9 +173,9 @@ describe ProjectWiki do
expect(subject.find_file('non-existant')).to eq(nil)
end
- it 'returns a Gollum::File instance' do
+ it 'returns a Gitlab::Git::WikiFile instance' do
file = subject.find_file('image.jpg')
- expect(file).to be_a Gollum::File
+ expect(file).to be_a Gitlab::Git::WikiFile
end
end
@@ -222,9 +217,9 @@ describe ProjectWiki do
describe "#update_page" do
before do
create_page("update-page", "some content")
- @gollum_page = subject.wiki.paged("update-page")
+ @gitlab_git_wiki_page = subject.wiki.page(title: "update-page")
subject.update_page(
- @gollum_page,
+ @gitlab_git_wiki_page,
content: "some other content",
format: :markdown,
message: "updated page"
@@ -246,7 +241,7 @@ describe ProjectWiki do
it 'updates project activity' do
subject.update_page(
- @gollum_page,
+ @gitlab_git_wiki_page,
content: 'Yet more content',
format: :markdown,
message: 'Updated page again'
@@ -262,7 +257,7 @@ describe ProjectWiki do
describe "#delete_page" do
before do
create_page("index", "some content")
- @page = subject.wiki.paged("index")
+ @page = subject.wiki.page(title: "index")
end
it "deletes the page" do
@@ -282,27 +277,28 @@ describe ProjectWiki do
describe '#create_repo!' do
it 'creates a repository' do
- expect(subject).to receive(:init_repo)
- .with(subject.full_path)
- .and_return(true)
-
+ expect(raw_repository.exists?).to eq(false)
expect(subject.repository).to receive(:after_create)
- expect(subject.create_repo!).to be_an_instance_of(Gollum::Wiki)
+ subject.send(:create_repo!, raw_repository)
+
+ expect(raw_repository.exists?).to eq(true)
end
end
describe '#ensure_repository' do
it 'creates the repository if it not exist' do
- allow(subject).to receive(:repository_exists?).and_return(false)
-
- expect(subject).to receive(:create_repo!)
+ expect(raw_repository.exists?).to eq(false)
+ expect(subject).to receive(:create_repo!).and_call_original
subject.ensure_repository
+
+ expect(raw_repository.exists?).to eq(true)
end
it 'does not create the repository if it exists' do
- allow(subject).to receive(:repository_exists?).and_return(true)
+ subject.wiki
+ expect(raw_repository.exists?).to eq(true)
expect(subject).not_to receive(:create_repo!)
@@ -329,7 +325,7 @@ describe ProjectWiki do
end
def commit_details
- { name: user.name, email: user.email, message: "test commit" }
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
end
def create_page(name, content)
@@ -337,6 +333,6 @@ describe ProjectWiki do
end
def destroy_page(page)
- subject.wiki.delete_page(page, commit_details)
+ subject.delete_page(page, commit_details)
end
end
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
index 532fb024261..ad3c3a406d9 100644
--- a/spec/models/push_event_spec.rb
+++ b/spec/models/push_event_spec.rb
@@ -11,6 +11,94 @@ describe PushEvent do
event
end
+ describe '.created_or_pushed' do
+ let(:event1) { create(:push_event) }
+ let(:event2) { create(:push_event) }
+ let(:event3) { create(:push_event) }
+
+ before do
+ create(:push_event_payload, event: event1, action: :pushed)
+ create(:push_event_payload, event: event2, action: :created)
+ create(:push_event_payload, event: event3, action: :removed)
+ end
+
+ let(:relation) { described_class.created_or_pushed }
+
+ it 'includes events for pushing to existing refs' do
+ expect(relation).to include(event1)
+ end
+
+ it 'includes events for creating new refs' do
+ expect(relation).to include(event2)
+ end
+
+ it 'does not include events for removing refs' do
+ expect(relation).not_to include(event3)
+ end
+ end
+
+ describe '.branch_events' do
+ let(:event1) { create(:push_event) }
+ let(:event2) { create(:push_event) }
+
+ before do
+ create(:push_event_payload, event: event1, ref_type: :branch)
+ create(:push_event_payload, event: event2, ref_type: :tag)
+ end
+
+ let(:relation) { described_class.branch_events }
+
+ it 'includes events for branches' do
+ expect(relation).to include(event1)
+ end
+
+ it 'does not include events for tags' do
+ expect(relation).not_to include(event2)
+ end
+ end
+
+ describe '.without_existing_merge_requests' do
+ let(:project) { create(:project, :repository) }
+ let(:event1) { create(:push_event, project: project) }
+ let(:event2) { create(:push_event, project: project) }
+ let(:event3) { create(:push_event, project: project) }
+ let(:event4) { create(:push_event, project: project) }
+
+ before do
+ create(:push_event_payload, event: event1, ref: 'foo', action: :created)
+ create(:push_event_payload, event: event2, ref: 'bar', action: :created)
+ create(:push_event_payload, event: event3, ref: 'baz', action: :removed)
+ create(:push_event_payload, event: event4, ref: 'baz', ref_type: :tag)
+
+ project.repository.create_branch('bar', 'master')
+
+ create(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'bar'
+ )
+ end
+
+ let(:relation) { described_class.without_existing_merge_requests }
+
+ it 'includes events that do not have a corresponding merge request' do
+ expect(relation).to include(event1)
+ end
+
+ it 'does not include events that have a corresponding merge request' do
+ expect(relation).not_to include(event2)
+ end
+
+ it 'does not include events for removed refs' do
+ expect(relation).not_to include(event3)
+ end
+
+ it 'does not include events for pushing to tags' do
+ expect(relation).not_to include(event4)
+ end
+ end
+
describe '.sti_name' do
it 'returns Event::PUSHED' do
expect(described_class.sti_name).to eq(Event::PUSHED)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7065d467ec0..5d78aed5b4f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Repository, models: true do
+describe Repository do
include RepoHelpers
TestBlob = Struct.new(:path)
@@ -8,12 +8,9 @@ describe Repository, models: true do
let(:repository) { project.repository }
let(:broken_repository) { create(:project, :broken_storage).repository }
let(:user) { create(:user) }
- let(:committer) { Gitlab::Git::Committer.from_user(user) }
+ let(:git_user) { Gitlab::Git::User.from_gitlab(user) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
+ let(:message) { 'Test message' }
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
@@ -21,7 +18,7 @@ describe Repository, models: true do
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ message)
repository.commit(merge_commit_id)
end
@@ -43,7 +40,7 @@ describe Repository, models: true do
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
@@ -161,7 +158,7 @@ describe Repository, models: true do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
@@ -174,7 +171,7 @@ describe Repository, models: true do
it_behaves_like 'getting last commit for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit for path'
end
end
@@ -195,7 +192,7 @@ describe Repository, models: true do
is_expected.to eq('c1acaa5')
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
@@ -208,7 +205,7 @@ describe Repository, models: true do
it_behaves_like 'getting last commit ID for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit ID for path'
end
end
@@ -258,11 +255,11 @@ describe Repository, models: true do
it_behaves_like 'finding commits by message'
end
- context 'when Gitaly commits_by_message feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding commits by message'
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
@@ -592,7 +589,7 @@ describe Repository, models: true do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
@@ -629,7 +626,7 @@ describe Repository, models: true do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
@@ -637,20 +634,24 @@ describe Repository, models: true do
end
describe '#fetch_ref' do
- describe 'when storage is broken', broken_storage: true do
- it 'should raise a storage error' do
- path = broken_repository.path_to_repo
+ # Setting the var here, sidesteps the stub that makes gitaly raise an error
+ # before the actual test call
+ set(:broken_repository) { create(:project, :broken_storage).repository }
- expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') }
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
+ end
end
end
end
describe '#create_ref' do
- it 'redirects the call to fetch_ref' do
+ it 'redirects the call to write_ref' do
ref, ref_path = '1', '2'
- expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
+ expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref)
repository.create_ref(ref, ref_path)
end
@@ -818,45 +819,70 @@ describe Repository, models: true do
end
describe '#add_branch' do
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- hook = double(trigger: [true, nil])
- expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ let(:branch_name) { 'new_feature' }
+ let(:target) { 'master' }
- expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
- end
+ subject { repository.add_branch(user, branch_name, target) }
- it 'creates the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+ context 'with Gitaly enabled' do
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_create_branch).with(branch_name, user, target)
+ .and_return(nil)
- branch = repository.add_branch(user, 'new_feature', 'master')
+ subject
+ end
- expect(branch.name).to eq('new_feature')
+ it 'creates_the_branch' do
+ expect(subject.name).to eq(branch_name)
+ expect(repository.find_branch(branch_name)).not_to be_nil
end
- it 'calls the after_create_branch hook' do
- expect(repository).to receive(:after_create_branch)
+ context 'with a non-existing target' do
+ let(:target) { 'fake-target' }
- repository.add_branch(user, 'new_feature', 'master')
+ it "returns false and doesn't create the branch" do
+ expect(subject).to be(false)
+ expect(repository.find_branch(branch_name)).to be_nil
+ end
end
end
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+ context 'with Gitaly disabled', :skip_gitaly_mock do
+ context 'when pre hooks were successful' do
+ it 'runs without errors' do
+ hook = double(trigger: [true, nil])
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect do
- repository.add_branch(user, 'new_feature', 'master')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ expect { subject }.not_to raise_error
+ end
+
+ it 'creates the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+
+ expect(subject.name).to eq(branch_name)
+ end
+
+ it 'calls the after_create_branch hook' do
+ expect(repository).to receive(:after_create_branch)
+
+ subject
+ end
end
- it 'does not create the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+ context 'when pre hooks failed' do
+ it 'gets an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
- expect do
- repository.add_branch(user, 'new_feature', 'master')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch('new_feature')).to be_nil
+ expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end
+
+ it 'does not create the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ expect(repository.find_branch(branch_name)).to be_nil
+ end
end
end
end
@@ -879,47 +905,6 @@ describe Repository, models: true do
end
end
- describe '#rm_branch' do
- let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
- let(:blank_sha) { '0000000000000000000000000000000000000000' }
-
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
- end
-
- it 'deletes the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
-
- expect(repository.find_branch('feature')).to be_nil
- end
- end
-
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- end
-
- it 'does not delete the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch('feature')).not_to be_nil
- end
- end
- end
-
describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
@@ -932,20 +917,20 @@ describe Repository, models: true do
service = Gitlab::Git::HooksService.new
expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref)
+ .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref)
.and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
- service = Gitlab::Git::OperationService.new(committer, repository.raw_repository)
+ service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository)
expect(service).to receive(:update_autocrlf_option)
@@ -956,7 +941,7 @@ describe Repository, models: true do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
@@ -974,7 +959,7 @@ describe Repository, models: true do
expect(target_project.repository.raw_repository).to receive(:fetch_ref)
.and_call_original
- Gitlab::Git::OperationService.new(committer, target_repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
.with_branch(
'master',
start_repository: project.repository.raw_repository,
@@ -990,7 +975,7 @@ describe Repository, models: true do
it 'does not fetch_ref and just pass the commit' do
expect(target_repository).not_to receive(:fetch_ref)
- Gitlab::Git::OperationService.new(committer, target_repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
.with_branch('feature', start_repository: project.repository.raw_repository) { new_rev }
end
end
@@ -1009,7 +994,7 @@ describe Repository, models: true do
end
expect do
- Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository)
.with_branch('feature',
start_repository: project.repository.raw_repository,
&:itself)
@@ -1031,7 +1016,7 @@ describe Repository, models: true do
repository.add_branch(user, branch, old_rev)
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
new_rev
end
end.not_to raise_error
@@ -1049,7 +1034,7 @@ describe Repository, models: true do
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
new_rev
end
end.to raise_error(Gitlab::Git::CommitError)
@@ -1061,7 +1046,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
@@ -1116,7 +1101,7 @@ describe Repository, models: true do
expect(repository.exists?).to eq(false)
end
- context 'with broken storage', broken_storage: true do
+ context 'with broken storage', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.exists? }
end
@@ -1128,27 +1113,37 @@ describe Repository, models: true do
it_behaves_like 'repo exists check'
end
- context 'when repository_exists is enabled', skip_gitaly_mock: true do
+ context 'when repository_exists is enabled', :skip_gitaly_mock do
it_behaves_like 'repo exists check'
end
end
describe '#has_visible_content?' do
- subject { repository.has_visible_content? }
+ before do
+ # If raw_repository.has_visible_content? gets called more than once then
+ # caching is broken. We don't want that.
+ expect(repository.raw_repository).to receive(:has_visible_content?)
+ .once
+ .and_return(result)
+ end
- describe 'when there are no branches' do
- before do
- allow(repository.raw_repository).to receive(:branch_count).and_return(0)
- end
+ context 'when true' do
+ let(:result) { true }
- it { is_expected.to eq(false) }
+ it 'returns true and caches it' do
+ expect(repository.has_visible_content?).to eq(true)
+ # Second call hits the cache
+ expect(repository.has_visible_content?).to eq(true)
+ end
end
- describe 'when there are branches' do
- it 'returns true' do
- expect(repository.raw_repository).to receive(:branch_count).and_return(3)
+ context 'when false' do
+ let(:result) { false }
- expect(subject).to eq(true)
+ it 'returns false and caches it' do
+ expect(repository.has_visible_content?).to eq(false)
+ # Second call hits the cache
+ expect(repository.has_visible_content?).to eq(false)
end
end
end
@@ -1265,6 +1260,7 @@ describe Repository, models: true do
allow(repository).to receive(:empty?).and_return(true)
expect(cache).to receive(:expire).with(:empty?)
+ expect(cache).to receive(:expire).with(:has_visible_content?)
repository.expire_emptiness_caches
end
@@ -1273,6 +1269,7 @@ describe Repository, models: true do
allow(repository).to receive(:empty?).and_return(false)
expect(cache).not_to receive(:expire).with(:empty?)
+ expect(cache).not_to receive(:expire).with(:has_visible_content?)
repository.expire_emptiness_caches
end
@@ -1287,10 +1284,7 @@ describe Repository, models: true do
describe '#merge' do
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test \r\n\r\n message', committer: author, author: author }
- end
+ let(:message) { 'Test \r\n\r\n message' }
it 'merges the code and returns the commit id' do
expect(merge_commit).to be_present
@@ -1298,43 +1292,72 @@ describe Repository, models: true do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
it 'removes carriage returns from commit message' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
+
+ expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
+ end
- expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ def merge(repository, user, merge_request, message)
+ repository.merge(user, merge_request.diff_head_sha, merge_request, message)
+ end
+ end
+
+ describe '#ff_merge' do
+ before do
+ repository.add_branch(user, 'ff-target', 'feature~5')
end
- def merge(repository, user, merge_request, options = {})
- repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ it 'merges the code and return the commit id' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
+ merge_commit_id = repository.ff_merge(user,
+ merge_request.diff_head_sha,
+ merge_request.target_branch,
+ merge_request: merge_request)
+ merge_commit = repository.commit(merge_commit_id)
+
+ expect(merge_commit).to be_present
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
+
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
+ merge_commit_id = repository.ff_merge(user,
+ merge_request.diff_head_sha,
+ merge_request.target_branch,
+ merge_request: merge_request)
+
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
end
describe '#revert' do
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:message) { 'revert message' }
context 'when there is a conflict' do
it 'raises an error' do
- expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit was already reverted' do
it 'raises an error' do
- repository.revert(user, update_image_commit, 'master')
+ repository.revert(user, update_image_commit, 'master', message)
- expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit can be reverted' do
it 'reverts the changes' do
- expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
+ expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
end
end
@@ -1343,7 +1366,7 @@ describe Repository, models: true do
merge_commit
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
- repository.revert(user, merge_commit, 'master')
+ repository.revert(user, merge_commit, 'master', message)
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
end
end
@@ -1353,24 +1376,25 @@ describe Repository, models: true do
let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+ let(:message) { 'cherry-pick message' }
context 'when there is a conflict' do
it 'raises an error' do
- expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit was already cherry-picked' do
it 'raises an error' do
- repository.cherry_pick(user, pickable_commit, 'master')
+ repository.cherry_pick(user, pickable_commit, 'master', message)
- expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit can be cherry-picked' do
it 'cherry-picks the changes' do
- expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
+ expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
end
end
@@ -1378,11 +1402,11 @@ describe Repository, models: true do
it 'cherry-picks the changes' do
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
- expect(cherry_pick_commit_message).to include('cherry picked from')
+ expect(cherry_pick_commit_message).to eq(message)
end
end
end
@@ -1603,7 +1627,7 @@ describe Repository, models: true do
describe '#expire_branches_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(branch_names branch_count))
+ .with(%i(branch_names branch_count has_visible_content?))
.and_call_original
repository.expire_branches_cache
@@ -1621,27 +1645,41 @@ describe Repository, models: true do
end
describe '#add_tag' do
- context 'with a valid target' do
- let(:user) { build_stubbed(:user) }
+ let(:user) { build_stubbed(:user) }
- it 'creates the tag using rugged' do
- expect(repository.rugged.tags).to receive(:create)
- .with('8.5', repository.commit('master').id,
- hash_including(message: 'foo',
- tagger: hash_including(name: user.name, email: user.email)))
- .and_call_original
+ shared_examples 'adding tag' do
+ context 'with a valid target' do
+ it 'creates the tag' do
+ repository.add_tag(user, '8.5', 'master', 'foo')
- repository.add_tag(user, '8.5', 'master', 'foo')
- end
+ tag = repository.find_tag('8.5')
+ expect(tag).to be_present
+ expect(tag.message).to eq('foo')
+ expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
+ end
- it 'returns a Gitlab::Git::Tag object' do
- tag = repository.add_tag(user, '8.5', 'master', 'foo')
+ it 'returns a Gitlab::Git::Tag object' do
+ tag = repository.add_tag(user, '8.5', 'master', 'foo')
+
+ expect(tag).to be_a(Gitlab::Git::Tag)
+ end
+ end
- expect(tag).to be_a(Gitlab::Git::Tag)
+ context 'with an invalid target' do
+ it 'returns false' do
+ expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
+ end
end
+ end
- it 'passes commit SHA to pre-receive and update hooks,\
- and tag SHA to post-receive hook' do
+ context 'when Gitaly operation_user_add_tag feature is enabled' do
+ it_behaves_like 'adding tag'
+ end
+
+ context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do
+ it_behaves_like 'adding tag'
+
+ it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
update_hook = Gitlab::Git::Hook.new('update', project)
post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
@@ -1659,39 +1697,105 @@ describe Repository, models: true do
tag_sha = tag.target
expect(pre_receive_hook).to have_received(:trigger)
- .with(anything, anything, commit_sha, anything)
+ .with(anything, anything, anything, commit_sha, anything)
expect(update_hook).to have_received(:trigger)
- .with(anything, anything, commit_sha, anything)
+ .with(anything, anything, anything, commit_sha, anything)
expect(post_receive_hook).to have_received(:trigger)
- .with(anything, anything, tag_sha, anything)
+ .with(anything, anything, anything, tag_sha, anything)
end
end
+ end
- context 'with an invalid target' do
- it 'returns false' do
- expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
+ describe '#rm_branch' do
+ shared_examples "user deleting a branch" do
+ it 'removes a branch' do
+ expect(repository).to receive(:before_remove_branch)
+ expect(repository).to receive(:after_remove_branch)
+
+ repository.rm_branch(user, 'feature')
end
end
- end
- describe '#rm_branch' do
- let(:user) { create(:user) }
+ context 'with gitaly enabled' do
+ it_behaves_like "user deleting a branch"
+
+ context 'when pre hooks failed' do
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
+ end
+
+ it 'gets an error and does not delete the branch' do
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
+ end
+
+ context 'with gitaly disabled', :skip_gitaly_mock do
+ it_behaves_like "user deleting a branch"
+
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:blank_sha) { '0000000000000000000000000000000000000000' }
+
+ context 'when pre hooks were successful' do
+ it 'runs without errors' do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
+ .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+ end
+
+ it 'deletes the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
- it 'removes a branch' do
- expect(repository).to receive(:before_remove_branch)
- expect(repository).to receive(:after_remove_branch)
+ expect(repository.find_branch('feature')).to be_nil
+ end
+ end
+
+ context 'when pre hooks failed' do
+ it 'gets an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end
+
+ it 'does not delete the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
- repository.rm_branch(user, 'feature')
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
end
end
describe '#rm_tag' do
- it 'removes a tag' do
- expect(repository).to receive(:before_remove_tag)
+ shared_examples 'removing tag' do
+ it 'removes a tag' do
+ expect(repository).to receive(:before_remove_tag)
- repository.rm_tag(create(:user), 'v1.1.0')
+ repository.rm_tag(build_stubbed(:user), 'v1.1.0')
- expect(repository.find_tag('v1.1.0')).to be_nil
+ expect(repository.find_tag('v1.1.0')).to be_nil
+ end
+ end
+
+ context 'when Gitaly operation_user_delete_tag feature is enabled' do
+ it_behaves_like 'removing tag'
+ end
+
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
+ it_behaves_like 'removing tag'
end
end
@@ -1864,6 +1968,15 @@ describe Repository, models: true do
repository.expire_all_method_caches
end
+
+ it 'all cache_method definitions are in the lists of method caches' do
+ methods = repository.methods.map do |method|
+ match = /^_uncached_(.*)/.match(method)
+ match[1].to_sym if match
+ end.compact
+
+ expect(methods).to match_array(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS)
+ end
end
describe '#file_on_head' do
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 8f05deb8b15..5ec04b99957 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
describe SentNotification do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
describe 'validation' do
describe 'note validity' do
context "when the project doesn't match the noteable's project" do
@@ -34,7 +37,6 @@ describe SentNotification do
end
describe '.record' do
- let(:user) { create(:user) }
let(:issue) { create(:issue) }
it 'creates a new SentNotification' do
@@ -43,7 +45,6 @@ describe SentNotification do
end
describe '.record_note' do
- let(:user) { create(:user) }
let(:note) { create(:diff_note_on_merge_request) }
it 'creates a new SentNotification' do
@@ -51,6 +52,123 @@ describe SentNotification do
end
end
+ describe '#unsubscribable?' do
+ shared_examples 'an unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_unsubscribable }
+ end
+ end
+
+ shared_examples 'a non-unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_unsubscribable }
+ end
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_commit?' do
+ shared_examples 'a commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_commit }
+ end
+ end
+
+ shared_examples 'a non-commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_commit }
+ end
+ end
+
+ it_behaves_like 'a non-commit notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a commit notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-commit notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_snippet?' do
+ shared_examples 'a snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_snippet }
+ end
+ end
+
+ shared_examples 'a non-snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_snippet }
+ end
+ end
+
+ it_behaves_like 'a non-snippet notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a snippet notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a snippet notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
describe '#create_reply' do
context 'for issue' do
let(:issue) { create(:issue) }
diff --git a/spec/models/user_custom_attribute_spec.rb b/spec/models/user_custom_attribute_spec.rb
new file mode 100644
index 00000000000..37fc3cb64f0
--- /dev/null
+++ b/spec/models/user_custom_attribute_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe UserCustomAttribute do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ subject { build :user_custom_attribute }
+
+ it { is_expected.to validate_presence_of(:user_id) }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_presence_of(:value) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:user_id) }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 73a1e47149c..1c3c9068f12 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do
include Gitlab::CurrentSettings
+ include ProjectForksHelper
describe 'modules' do
subject { described_class }
@@ -39,6 +40,7 @@ describe User do
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
+ it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
describe "#abuse_report" do
let(:current_user) { create(:user) }
@@ -359,9 +361,22 @@ describe User do
expect(external_user.projects_limit).to be 0
end
end
+
+ describe '#check_for_verified_email' do
+ let(:user) { create(:user) }
+ let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
+
+ it 'allows a verfied secondary email to be used as the primary without needing reconfirmation' do
+ user.update_attributes!(email: secondary.email)
+ user.reload
+ expect(user.email).to eq secondary.email
+ expect(user.unconfirmed_email).to eq nil
+ expect(user.confirmed?).to be_truthy
+ end
+ end
end
- describe 'after update hook' do
+ describe 'after commit hook' do
describe '.update_invalid_gpg_signatures' do
let(:user) do
create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
@@ -375,10 +390,50 @@ describe User do
end
it 'synchronizes the gpg keys when the email is updated' do
- expect(user).to receive(:update_invalid_gpg_signatures)
+ expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
end
end
+
+ describe '#update_emails_with_primary_email' do
+ before do
+ @user = create(:user, email: 'primary@example.com').tap do |user|
+ user.skip_reconfirmation!
+ end
+ @secondary = create :email, email: 'secondary@example.com', user: @user
+ @user.reload
+ end
+
+ it 'gets called when email updated' do
+ expect(@user).to receive(:update_emails_with_primary_email)
+
+ @user.update_attributes!(email: 'new_primary@example.com')
+ end
+
+ it 'adds old primary to secondary emails when secondary is a new email ' do
+ @user.update_attributes!(email: 'new_primary@example.com')
+ @user.reload
+
+ expect(@user.emails.count).to eq 2
+ expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com'])
+ end
+
+ it 'adds old primary to secondary emails if secondary is becoming a primary' do
+ @user.update_attributes!(email: @secondary.email)
+ @user.reload
+
+ expect(@user.emails.count).to eq 1
+ expect(@user.emails.first.email).to eq 'primary@example.com'
+ end
+
+ it 'transfers old confirmation values into new secondary' do
+ @user.update_attributes!(email: @secondary.email)
+ @user.reload
+
+ expect(@user.emails.count).to eq 1
+ expect(@user.emails.first.confirmed_at).not_to eq nil
+ end
+ end
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
@@ -466,6 +521,7 @@ describe User do
describe '#generate_password' do
it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
+
expect(user.password).to eq('abcdefghe')
end
end
@@ -473,6 +529,7 @@ describe User do
describe 'authentication token' do
it "has authentication token" do
user = create(:user)
+
expect(user.authentication_token).not_to be_blank
end
end
@@ -480,6 +537,7 @@ describe User do
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
+
expect(user.incoming_email_token).not_to be_blank
end
end
@@ -522,6 +580,7 @@ describe User do
it 'ensures an rss token on read' do
user = create(:user, rss_token: nil)
rss_token = user.rss_token
+
expect(rss_token).not_to be_blank
expect(user.reload.rss_token).to eq rss_token
end
@@ -632,6 +691,7 @@ describe User do
it "blocks user" do
user.block
+
expect(user.blocked?).to be_truthy
end
end
@@ -965,6 +1025,7 @@ describe User do
it 'is case-insensitive' do
user = create(:user, username: 'JohnDoe')
+
expect(described_class.find_by_username('JOHNDOE')).to eq user
end
end
@@ -977,6 +1038,7 @@ describe User do
it 'is case-insensitive' do
user = create(:user, username: 'JohnDoe')
+
expect(described_class.find_by_username!('JOHNDOE')).to eq user
end
end
@@ -1066,11 +1128,13 @@ describe User do
it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
+
expect(user.avatar_type).to be_truthy
end
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
+
expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -1093,6 +1157,50 @@ describe User do
end
end
+ describe '#all_emails' do
+ let(:user) { create(:user) }
+
+ it 'returns all emails' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ email_unconfirmed = create :email, user: user
+ user.reload
+
+ expect(user.all_emails).to match_array([user.email, email_unconfirmed.email, email_confirmed.email])
+ end
+ end
+
+ describe '#verified_emails' do
+ let(:user) { create(:user) }
+
+ it 'returns only confirmed emails' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ create :email, user: user
+ user.reload
+
+ expect(user.verified_emails).to match_array([user.email, email_confirmed.email])
+ end
+ end
+
+ describe '#verified_email?' do
+ let(:user) { create(:user) }
+
+ it 'returns true when the email is verified/confirmed' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ create :email, user: user
+ user.reload
+
+ expect(user.verified_email?(user.email)).to be_truthy
+ expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy
+ end
+
+ it 'returns false when the email is not verified/confirmed' do
+ email_unconfirmed = create :email, user: user
+ user.reload
+
+ expect(user.verified_email?(email_unconfirmed.email)).to be_falsy
+ end
+ end
+
describe '#requires_ldap_check?' do
let(:user) { described_class.new }
@@ -1100,6 +1208,7 @@ describe User do
# Create a condition which would otherwise cause 'true' to be returned
allow(user).to receive(:ldap_user?).and_return(true)
user.last_credential_check_at = nil
+
expect(user.requires_ldap_check?).to be_falsey
end
@@ -1110,6 +1219,7 @@ describe User do
it 'is false for non-LDAP users' do
allow(user).to receive(:ldap_user?).and_return(false)
+
expect(user.requires_ldap_check?).to be_falsey
end
@@ -1120,11 +1230,13 @@ describe User do
it 'is true when the user has never had an LDAP check before' do
user.last_credential_check_at = nil
+
expect(user.requires_ldap_check?).to be_truthy
end
it 'is true when the last LDAP check happened over 1 hour ago' do
user.last_credential_check_at = 2.hours.ago
+
expect(user.requires_ldap_check?).to be_truthy
end
end
@@ -1135,16 +1247,19 @@ describe User do
describe '#ldap_user?' do
it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain')
+
expect(user.ldap_user?).to be_truthy
end
it 'is false for other providers' do
user = create(:omniauth_user, provider: 'other-provider')
+
expect(user.ldap_user?).to be_falsey
end
it 'is false if no extern_uid is provided' do
user = create(:omniauth_user, extern_uid: nil)
+
expect(user.ldap_user?).to be_falsey
end
end
@@ -1152,6 +1267,7 @@ describe User do
describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
+
expect(user.ldap_identity.provider).not_to be_empty
end
end
@@ -1161,6 +1277,7 @@ describe User do
it 'blocks user flaging the action caming from ldap' do
user.ldap_block
+
expect(user.blocked?).to be_truthy
expect(user.ldap_blocked?).to be_truthy
end
@@ -1233,18 +1350,22 @@ describe User do
expect(user.starred?(project2)).to be_falsey
star1 = UsersStarProject.create!(project: project1, user: user)
+
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_falsey
star2 = UsersStarProject.create!(project: project2, user: user)
+
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_truthy
star1.destroy
+
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_truthy
star2.destroy
+
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
end
@@ -1256,9 +1377,13 @@ describe User do
project = create(:project, :public)
expect(user.starred?(project)).to be_falsey
+
user.toggle_star(project)
+
expect(user.starred?(project)).to be_truthy
+
user.toggle_star(project)
+
expect(user.starred?(project)).to be_falsey
end
end
@@ -1307,7 +1432,7 @@ describe User do
describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project3) }
+ let!(:project2) { fork_project(project3) }
let!(:project3) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:push_event, project: project1, author: subject) }
@@ -1331,6 +1456,23 @@ describe User do
end
end
+ describe '#fork_of' do
+ let(:user) { create(:user) }
+
+ it "returns a user's fork of a project" do
+ project = create(:project, :public)
+ user_fork = fork_project(project, user, namespace: user.namespace)
+
+ expect(user.fork_of(project)).to eq(user_fork)
+ end
+
+ it 'returns nil if the project does not have a fork network' do
+ project = create(:project)
+
+ expect(user.fork_of(project)).to be_nil
+ end
+ end
+
describe '#can_be_removed?' do
subject { create(:user) }
@@ -1349,56 +1491,24 @@ describe User do
end
describe "#recent_push" do
- subject { create(:user) }
- let!(:project1) { create(:project, :repository) }
- let!(:project2) { create(:project, :repository, forked_from_project: project1) }
-
- let!(:push_event) do
- event = create(:push_event, project: project2, author: subject)
-
- create(:push_event_payload,
- event: event,
- commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- commit_count: 0,
- ref: 'master')
-
- event
- end
-
- before do
- project1.team << [subject, :master]
- project2.team << [subject, :master]
- end
-
- it "includes push event" do
- expect(subject.recent_push).to eq(push_event)
- end
+ let(:user) { build(:user) }
+ let(:project) { build(:project) }
+ let(:event) { build(:push_event) }
- it "excludes push event if branch has been deleted" do
- allow_any_instance_of(Repository).to receive(:branch_exists?).with('master').and_return(false)
+ it 'returns the last push event for the user' do
+ expect_any_instance_of(Users::LastPushEventService)
+ .to receive(:last_event_for_user)
+ .and_return(event)
- expect(subject.recent_push).to eq(nil)
+ expect(user.recent_push).to eq(event)
end
- it "excludes push event if MR is opened for it" do
- create(:merge_request, source_project: project2, target_project: project1, source_branch: project2.default_branch, target_branch: 'fix', author: subject)
+ it 'returns the last push event for a project when one is given' do
+ expect_any_instance_of(Users::LastPushEventService)
+ .to receive(:last_event_for_project)
+ .and_return(event)
- expect(subject.recent_push).to eq(nil)
- end
-
- it "includes push events on any of the provided projects" do
- expect(subject.recent_push(project1)).to eq(nil)
- expect(subject.recent_push(project2)).to eq(push_event)
-
- push_event1 = create(:push_event, project: project1, author: subject)
-
- create(:push_event_payload,
- event: push_event1,
- commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- commit_count: 0,
- ref: 'master')
-
- expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
+ expect(user.recent_push(project)).to eq(event)
end
end
@@ -1415,7 +1525,7 @@ describe User do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects', truncate: true do
+ describe '#authorized_projects', :truncate do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -1469,9 +1579,11 @@ describe User do
user = create(:user)
member = group.add_developer(user)
+
expect(user.authorized_projects).to include(project)
member.destroy
+
expect(user.authorized_projects).not_to include(project)
end
@@ -1492,9 +1604,11 @@ describe User do
project = create(:project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
+
expect(user2.authorized_projects).to include(project)
project.destroy
+
expect(user2.authorized_projects).not_to include(project)
end
@@ -1504,9 +1618,11 @@ describe User do
user = create(:user)
group.add_developer(user)
+
expect(user.authorized_projects).to include(project)
group.destroy
+
expect(user.authorized_projects).not_to include(project)
end
end
@@ -1761,7 +1877,7 @@ describe User do
end
end
- describe '#refresh_authorized_projects', clean_gitlab_redis_shared_state: true do
+ describe '#refresh_authorized_projects', :clean_gitlab_redis_shared_state do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let(:user) { create(:user) }
@@ -2050,7 +2166,9 @@ describe User do
it 'creates the namespace' do
expect(user.namespace).to be_nil
+
user.save!
+
expect(user.namespace).not_to be_nil
end
end
@@ -2071,11 +2189,13 @@ describe User do
it 'updates the namespace name' do
user.update_attributes!(username: new_username)
+
expect(user.namespace.name).to eq(new_username)
end
it 'updates the namespace path' do
user.update_attributes!(username: new_username)
+
expect(user.namespace.path).to eq(new_username)
end
@@ -2089,6 +2209,7 @@ describe User do
it 'adds the namespace errors to the user' do
user.update_attributes(username: new_username)
+
expect(user.errors.full_messages.first).to eq('Namespace name has already been taken')
end
end
@@ -2105,56 +2226,49 @@ describe User do
end
end
- describe '#verified_email?' do
- it 'returns true when the email is the primary email' do
- user = build :user, email: 'email@example.com'
-
- expect(user.verified_email?('email@example.com')).to be true
- end
-
- it 'returns false when the email is not the primary email' do
- user = build :user, email: 'email@example.com'
-
- expect(user.verified_email?('other_email@example.com')).to be false
- end
- end
-
describe '#sync_attribute?' do
let(:user) { described_class.new }
context 'oauth user' do
it 'returns true if name can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(name location))
+
expect(user.sync_attribute?(:name)).to be_truthy
end
it 'returns true if email can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(name email))
+
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns true if location can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns false if name can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if email can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if location can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns true for all syncable attributes if all syncable attributes can be synced' do
stub_omniauth_setting(sync_profile_attributes: true)
+
expect(user.sync_attribute?(:name)).to be_truthy
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_truthy
@@ -2170,6 +2284,7 @@ describe User do
context 'ldap user' do
it 'returns true for email if ldap user' do
allow(user).to receive(:ldap_user?).and_return(true)
+
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_falsey
@@ -2178,10 +2293,56 @@ describe User do
it 'returns true for email and location if ldap user and location declared as syncable' do
allow(user).to receive(:ldap_user?).and_return(true)
stub_omniauth_setting(sync_profile_attributes: %w(location))
+
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_truthy
end
end
end
+
+ describe '#confirm_deletion_with_password?' do
+ where(
+ password_automatically_set: [true, false],
+ ldap_user: [true, false],
+ password_authentication_disabled: [true, false]
+ )
+
+ with_them do
+ let!(:user) { create(:user, password_automatically_set: password_automatically_set) }
+ let!(:identity) { create(:identity, user: user) if ldap_user }
+
+ # Only confirm deletion with password if all inputs are false
+ let(:expected) { !(password_automatically_set || ldap_user || password_authentication_disabled) }
+
+ before do
+ stub_application_setting(password_authentication_enabled: !password_authentication_disabled)
+ end
+
+ it 'returns false unless all inputs are true' do
+ expect(user.confirm_deletion_with_password?).to eq(expected)
+ end
+ end
+ end
+
+ describe '#delete_async' do
+ let(:user) { create(:user) }
+ let(:deleted_by) { create(:user) }
+
+ it 'blocks the user then schedules them for deletion if a hard delete is specified' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, hard_delete: true)
+
+ user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
+
+ expect(user).to be_blocked
+ end
+
+ it 'schedules user for deletion without blocking them' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, {})
+
+ user.delete_async(deleted_by: deleted_by)
+
+ expect(user).not_to be_blocked
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 9ef8d117123..1f14d06997e 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -80,7 +80,7 @@ describe WikiPage do
context "when initialized with an existing gollum page" do
before do
create_page("test page", "test content")
- @page = wiki.wiki.paged("test page")
+ @page = wiki.wiki.page(title: "test page")
@wiki_page = described_class.new(wiki, @page, true)
end
@@ -105,7 +105,7 @@ describe WikiPage do
end
it "sets the version attribute" do
- expect(@wiki_page.version).to be_a Gollum::Git::Commit
+ expect(@wiki_page.version).to be_a Gitlab::Git::WikiPageVersion
end
end
end
@@ -321,14 +321,14 @@ describe WikiPage do
end
it 'returns true when requesting an old version' do
- old_version = @page.versions.last.to_s
+ old_version = @page.versions.last.id
old_page = wiki.find_page('Update', old_version)
expect(old_page.historical?).to eq true
end
it 'returns false when requesting latest version' do
- latest_version = @page.versions.first.to_s
+ latest_version = @page.versions.first.id
latest_page = wiki.find_page('Update', latest_version)
expect(latest_page.historical?).to eq false
@@ -393,7 +393,7 @@ describe WikiPage do
end
def commit_details
- { name: user.name, email: user.email, message: "test commit" }
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
end
def create_page(name, content)
@@ -401,8 +401,8 @@ describe WikiPage do
end
def destroy_page(title)
- page = wiki.wiki.paged(title)
- wiki.wiki.delete_page(page, commit_details)
+ page = wiki.wiki.page(title: title)
+ wiki.delete_page(page, commit_details)
end
def get_slugs(page_or_dir)
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/gcp/cluster_policy_spec.rb
new file mode 100644
index 00000000000..e213aa3d557
--- /dev/null
+++ b/spec/policies/gcp/cluster_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPolicy, :models do
+ set(:project) { create(:project) }
+ set(:cluster) { create(:gcp_cluster, project: project) }
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, cluster) }
+
+ describe 'rules' do
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
+end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index a6bf70c1e09..5b8cf2e6ab5 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -51,4 +51,41 @@ describe GlobalPolicy do
end
end
end
+
+ describe "create fork" do
+ context "when user has not exceeded project limit" do
+ it { is_expected.to be_allowed(:create_fork) }
+ end
+
+ context "when user has exceeded project limit" do
+ let(:current_user) { create(:user, projects_limit: 0) }
+
+ it { is_expected.not_to be_allowed(:create_fork) }
+ end
+
+ context "when user is a master in a group" do
+ let(:group) { create(:group) }
+ let(:current_user) { create(:user, projects_limit: 0) }
+
+ before do
+ group.add_master(current_user)
+ end
+
+ it { is_expected.to be_allowed(:create_fork) }
+ end
+ end
+
+ describe 'custom attributes' do
+ context 'regular user' do
+ it { is_expected.not_to be_allowed(:read_custom_attribute) }
+ it { is_expected.not_to be_allowed(:update_custom_attribute) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:user, :admin) }
+
+ it { is_expected.to be_allowed(:read_custom_attribute) }
+ it { is_expected.to be_allowed(:update_custom_attribute) }
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index b186a78e44a..17dc3bb4f48 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -11,10 +11,11 @@ describe GroupPolicy do
let(:reporter_permissions) { [:admin_label] }
+ let(:developer_permissions) { [:admin_milestones] }
+
let(:master_permissions) do
[
- :create_projects,
- :admin_milestones
+ :create_projects
]
end
@@ -52,6 +53,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -63,6 +65,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -74,6 +77,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -85,6 +89,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -96,6 +101,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -109,6 +115,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
@@ -122,6 +129,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
@@ -180,6 +188,7 @@ describe GroupPolicy do
it do
expect_disallowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -191,6 +200,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -202,6 +212,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -213,6 +224,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -224,6 +236,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -237,6 +250,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
new file mode 100644
index 00000000000..2cf669e8191
--- /dev/null
+++ b/spec/policies/issuable_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe IssuablePolicy, models: true do
+ describe '#rules' do
+ context 'when discussion is locked for the issuable' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, discussion_locked: true) }
+ let(:policies) { described_class.new(user, issue) }
+
+ context 'when the user is not a project member' do
+ it 'can not create a note' do
+ expect(policies).to be_disallowed(:create_note)
+ end
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'can create a note' do
+ expect(policies).to be_allowed(:create_note)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
new file mode 100644
index 00000000000..e52ff02e5f0
--- /dev/null
+++ b/spec/policies/namespace_policy_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe NamespacePolicy do
+ let(:current_user) { create(:user) }
+ let(:namespace) { current_user.namespace }
+
+ subject { described_class.new(current_user, namespace) }
+
+ context "create projects" do
+ context "user namespace" do
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context "user who has exceeded project limit" do
+ let(:current_user) { create(:user, projects_limit: 0) }
+
+ it { is_expected.not_to be_allowed(:create_projects) }
+ end
+ end
+end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
new file mode 100644
index 00000000000..58d36a2c84e
--- /dev/null
+++ b/spec/policies/note_policy_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe NotePolicy, mdoels: true do
+ describe '#rules' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ def policies(noteable = nil)
+ return @policies if @policies
+
+ noteable ||= issue
+ note = create(:note, noteable: noteable, author: user, project: project)
+
+ @policies = described_class.new(user, note)
+ end
+
+ context 'when the project is public' do
+ context 'when the note author is not a project member' do
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the noteable is a snippet' do
+ it 'can edit note' do
+ policies = policies(create(:project_snippet, project: project))
+
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when a discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when the note author is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the note author is not a project member' do
+ it 'can not edit a note' do
+ expect(policies).to be_disallowed(:update_note)
+ expect(policies).to be_disallowed(:admin_note)
+ expect(policies).to be_disallowed(:resolve_note)
+ end
+
+ it 'can read a note' do
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 4dbaf7fb025..f2593a1a75c 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe ProjectPolicy do
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
- let(:dev) { create(:user) }
- let(:master) { create(:user) }
- let(:owner) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:guest) { create(:user) }
+ set(:reporter) { create(:user) }
+ set(:developer) { create(:user) }
+ set(:master) { create(:user) }
+ set(:owner) { create(:user) }
+ set(:admin) { create(:admin) }
let(:project) { create(:project, :public, namespace: owner.namespace) }
- let(:guest_permissions) do
+ let(:base_guest_permissions) do
%i[
read_project read_board read_list read_wiki read_issue read_label
read_milestone read_project_snippet read_project_member
@@ -18,7 +18,7 @@ describe ProjectPolicy do
]
end
- let(:reporter_permissions) do
+ let(:base_reporter_permissions) do
%i[
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
@@ -33,7 +33,7 @@ describe ProjectPolicy do
let(:developer_permissions) do
%i[
- admin_merge_request update_merge_request create_commit_status
+ admin_milestone admin_merge_request update_merge_request create_commit_status
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request create_wiki push_code
resolve_note create_container_image update_container_image
@@ -41,10 +41,10 @@ describe ProjectPolicy do
]
end
- let(:master_permissions) do
+ let(:base_master_permissions) do
%i[
delete_protected_branch update_project_snippet update_environment
- update_deployment admin_milestone admin_project_snippet
+ update_deployment admin_project_snippet
admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment
@@ -66,11 +66,20 @@ describe ProjectPolicy do
]
end
+ # Used in EE specs
+ let(:additional_guest_permissions) { [] }
+ let(:additional_reporter_permissions) { [] }
+ let(:additional_master_permissions) { [] }
+
+ let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
+ let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
+ let(:master_permissions) { base_master_permissions + additional_master_permissions }
+
before do
- project.team << [guest, :guest]
- project.team << [master, :master]
- project.team << [dev, :developer]
- project.team << [reporter, :reporter]
+ project.add_guest(guest)
+ project.add_master(master)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
end
def expect_allowed(*permissions)
@@ -127,38 +136,41 @@ describe ProjectPolicy do
end
end
- context 'when a project has pending invites, and the current user is anonymous' do
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, namespace: group) }
- let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] }
- let(:anonymous_permissions) { guest_permissions - user_permissions }
+ shared_examples 'project policies as anonymous' do
+ context 'abilities for public projects' do
+ context 'when a project has pending invites' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] }
+ let(:anonymous_permissions) { guest_permissions - user_permissions }
- subject { described_class.new(nil, project) }
+ subject { described_class.new(nil, project) }
- before do
- create(:group_member, :invited, group: group)
- end
+ before do
+ create(:group_member, :invited, group: group)
+ end
- it 'does not grant owner access' do
- expect_allowed(*anonymous_permissions)
- expect_disallowed(*user_permissions)
+ it 'does not grant owner access' do
+ expect_allowed(*anonymous_permissions)
+ expect_disallowed(*user_permissions)
+ end
+ end
end
- end
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
- subject { described_class.new(current_user, project) }
-
- context 'with no user' do
- let(:current_user) { nil }
+ subject { described_class.new(nil, project) }
it { is_expected.to be_banned }
end
+ end
- context 'guests' do
- let(:current_user) { guest }
+ shared_examples 'project policies as guest' do
+ subject { described_class.new(guest, project) }
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
let(:reporter_public_build_permissions) do
reporter_permissions - [:read_build, :read_pipeline]
end
@@ -179,7 +191,7 @@ describe ProjectPolicy do
end
end
- context 'public builds disabled' do
+ context 'when public builds disabled' do
before do
project.update(public_builds: false)
end
@@ -192,8 +204,7 @@ describe ProjectPolicy do
context 'when builds are disabled' do
before do
- project.project_feature.update(
- builds_access_level: ProjectFeature::DISABLED)
+ project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
end
it do
@@ -202,9 +213,13 @@ describe ProjectPolicy do
end
end
end
+ end
+
+ shared_examples 'project policies as reporter' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
- context 'reporter' do
- let(:current_user) { reporter }
+ subject { described_class.new(reporter, project) }
it do
expect_allowed(*guest_permissions)
@@ -216,9 +231,13 @@ describe ProjectPolicy do
expect_disallowed(*owner_permissions)
end
end
+ end
- context 'developer' do
- let(:current_user) { dev }
+ shared_examples 'project policies as developer' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(developer, project) }
it do
expect_allowed(*guest_permissions)
@@ -229,9 +248,13 @@ describe ProjectPolicy do
expect_disallowed(*owner_permissions)
end
end
+ end
+
+ shared_examples 'project policies as master' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
- context 'master' do
- let(:current_user) { master }
+ subject { described_class.new(master, project) }
it do
expect_allowed(*guest_permissions)
@@ -242,9 +265,13 @@ describe ProjectPolicy do
expect_disallowed(*owner_permissions)
end
end
+ end
+
+ shared_examples 'project policies as owner' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
- context 'owner' do
- let(:current_user) { owner }
+ subject { described_class.new(owner, project) }
it do
expect_allowed(*guest_permissions)
@@ -255,9 +282,13 @@ describe ProjectPolicy do
expect_allowed(*owner_permissions)
end
end
+ end
- context 'admin' do
- let(:current_user) { admin }
+ shared_examples 'project policies as admin' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(admin, project) }
it do
expect_allowed(*guest_permissions)
@@ -269,4 +300,12 @@ describe ProjectPolicy do
end
end
end
+
+ it_behaves_like 'project policies as anonymous'
+ it_behaves_like 'project policies as guest'
+ it_behaves_like 'project policies as reporter'
+ it_behaves_like 'project policies as developer'
+ it_behaves_like 'project policies as master'
+ it_behaves_like 'project policies as owner'
+ it_behaves_like 'project policies as admin'
end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index e4886a8f019..f7ceaf844be 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -51,4 +51,21 @@ describe Ci::PipelinePresenter do
end
end
end
+
+ context '#failure_reason' do
+ context 'when pipeline has failure reason' do
+ it 'represents a failure reason sentence' do
+ pipeline.failure_reason = :config_error
+
+ expect(presenter.failure_reason)
+ .to eq 'CI/CD YAML configuration error!'
+ end
+ end
+
+ context 'when pipeline does not have failure reason' do
+ it 'returns nil' do
+ expect(presenter.failure_reason).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/gcp/cluster_presenter_spec.rb
new file mode 100644
index 00000000000..8d86dc31582
--- /dev/null
+++ b/spec/presenters/gcp/cluster_presenter_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPresenter do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:gcp_cluster, project: project) }
+
+ subject(:presenter) do
+ described_class.new(cluster)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+
+ describe '#initialize' do
+ it 'takes a cluster and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes cluster' do
+ expect(presenter.cluster).to eq(cluster)
+ end
+
+ it 'forwards missing methods to cluster' do
+ expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ end
+ end
+
+ describe '#gke_cluster_url' do
+ subject { described_class.new(cluster).gke_cluster_url }
+
+ it { is_expected.to include(cluster.gcp_cluster_zone) }
+ it { is_expected.to include(cluster.gcp_cluster_name) }
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 2187be0190d..5e114434a67 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -300,6 +300,10 @@ describe MergeRequestPresenter do
described_class.new(resource, current_user: user).remove_wip_path
end
+ before do
+ allow(resource).to receive(:work_in_progress?).and_return(true)
+ end
+
context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true }
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 6bd17697c33..50d0f72f6bc 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
describe API::AccessRequests do
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:access_requester) { create(:user) }
- let(:stranger) { create(:user) }
+ set(:master) { create(:user) }
+ set(:developer) { create(:user) }
+ set(:access_requester) { create(:user) }
+ set(:stranger) { create(:user) }
- let(:project) do
+ set(:project) do
create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
@@ -14,7 +14,7 @@ describe API::AccessRequests do
end
end
- let(:group) do
+ set(:group) do
create(:group, :public, :access_requestable) do |group|
group.add_developer(developer)
group.add_owner(master)
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 593068b8cd7..7a0765c1fae 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::AwardEmoji do
- let(:user) { create(:user) }
- let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
- let!(:note) { create(:note, project: project, noteable: issue) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ set(:note) { create(:note, project: project, noteable: issue) }
before do
project.team << [user, :master]
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f698d5dddb3..fcfa4ddfbfe 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,34 +1,34 @@
require 'spec_helper'
describe API::Boards do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
-
- let!(:dev_label) do
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:admin) { create(:user, :admin) }
+ set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
- let!(:test_label) do
+ set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
- let!(:ux_label) do
+ set(:ux_label) do
create(:label, title: 'UX', color: '#FF0000', project: project)
end
- let!(:dev_list) do
+ set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
- let!(:test_list) do
+ set(:test_list) do
create(:list, label: test_label, position: 2)
end
- let!(:board) do
+ set(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
@@ -187,8 +187,11 @@ describe API::Boards do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ set(:owner) { create(:user) }
+
+ before do
+ project.update(namespace: owner.namespace)
+ end
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index cc794fad3a7..16b12446ed4 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe API::Branches do
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ set(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:branch_name) { 'feature' }
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
@@ -40,7 +40,9 @@ describe API::Branches do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
it_behaves_like 'repository branches'
end
@@ -118,7 +120,9 @@ describe API::Branches do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
it_behaves_like 'repository branch'
end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index b043a333d33..eacc575d97f 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe API::BroadcastMessages do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
+ set(:message) { create(:broadcast_message) }
describe 'GET /broadcast_messages' do
it 'returns a 401 for anonymous users' do
@@ -31,8 +32,6 @@ describe API::BroadcastMessages do
end
describe 'GET /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
get api("/broadcast_messages/#{message.id}")
@@ -103,8 +102,6 @@ describe API::BroadcastMessages do
end
describe 'PUT /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
put api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
@@ -155,8 +152,6 @@ describe API::BroadcastMessages do
end
describe 'DELETE /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
delete api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index f663719d28c..94462b4572d 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -491,6 +491,7 @@ describe API::Commits do
expect(json_response['stats']['deletions']).to eq(commit.stats.deletions)
expect(json_response['stats']['total']).to eq(commit.stats.total)
expect(json_response['status']).to be_nil
+ expect(json_response['last_pipeline']).to be_nil
end
context 'when ref does not exist' do
@@ -573,6 +574,10 @@ describe API::Commits do
expect(response).to have_http_status(200)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['status']).to eq('created')
+ expect(json_response['last_pipeline']['id']).to eq(pipeline.id)
+ expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref)
+ expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha)
+ expect(json_response['last_pipeline']['status']).to eq(pipeline.status)
end
context 'when pipeline succeeds' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 2361809e0e1..f8cd529a06c 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -20,6 +20,7 @@ describe API::Environments do
path path_with_namespace
star_count forks_count
created_at last_activity_at
+ avatar_url
)
get api("/projects/#{project.id}/environments", user)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 42f0079e173..1671a046fdf 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -159,11 +159,14 @@ describe API::Groups do
context 'when using owned in the request' do
it 'returns an array of groups the user owns' do
+ group1.add_master(user2)
+
get api('/groups', user2), owned: true
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(group2.name)
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index d4006fe71a2..862920ad7c3 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,4 +1,6 @@
require 'spec_helper'
+require 'raven/transports/dummy'
+require_relative '../../../config/initializers/sentry'
describe API::Helpers do
include API::APIGuard::HelperMethods
@@ -159,18 +161,25 @@ describe API::Helpers do
end
describe "when authenticating using a user's private token" do
- it "returns nil for an invalid token" do
+ it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
- it "returns nil for a user without access" do
+ it "returns a 401 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
+ end
+
+ it 'returns a 401 response for a user who is blocked' do
+ user.block!
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
+
+ expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do
@@ -193,24 +202,31 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
end
- it "returns nil for an invalid token" do
+ it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
- it "returns nil for a user without access" do
+ it "returns a 401 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
- it "returns nil for a token without the appropriate scope" do
+ it 'returns a 401 response for a user who is blocked' do
+ user.block!
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect { current_user }.to raise_error /401/
+ end
+
+ it "returns a 401 response for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do
@@ -226,14 +242,14 @@ describe API::Helpers do
personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to be_nil
+ expect { current_user }.to raise_error /401/
end
end
@@ -351,6 +367,18 @@ describe API::Helpers do
end
end
end
+
+ context 'when user is blocked' do
+ before do
+ user.block!
+ end
+
+ it 'changes current_user to sudo' do
+ set_env(admin, user.id)
+
+ expect(current_user).to eq(user)
+ end
+ end
end
context 'with regular user' do
@@ -450,10 +478,55 @@ describe API::Helpers do
allow(exception).to receive(:backtrace).and_return(caller)
expect_any_instance_of(self.class).to receive(:sentry_context)
- expect(Raven).to receive(:capture_exception).with(exception)
+ expect(Raven).to receive(:capture_exception).with(exception, extra: {})
handle_api_exception(exception)
end
+
+ context 'with a personal access token given' do
+ let(:token) { create(:personal_access_token, scopes: ['api'], user: user) }
+
+ # Regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/38571
+ it 'does not raise an additional exception because of missing `request`' do
+ # We need to stub at a lower level than #sentry_enabled? otherwise
+ # Sentry is not enabled when the request below is made, and the test
+ # would pass even without the fix
+ expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
+ expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
+
+ get api('/projects', personal_access_token: token)
+
+ # The 500 status is expected as we're testing a case where an exception
+ # is raised, but Grape shouldn't raise an additional exception
+ expect(response).to have_gitlab_http_status(500)
+ expect(json_response['message']).not_to include("undefined local variable or method `request'")
+ expect(json_response['message']).to start_with("\nRuntimeError (Runtime Error!):")
+ end
+ end
+
+ context 'extra information' do
+ # Sentry events are an array of the form [auth_header, data, options]
+ let(:event_data) { Raven.client.transport.events.first[1] }
+
+ before do
+ stub_application_setting(
+ sentry_enabled: true,
+ sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
+ )
+ configure_sentry
+ Raven.client.configuration.encoding = 'json'
+ end
+
+ it 'sends the params, excluding confidential values' do
+ expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
+ expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
+
+ get api('/projects', user), password: 'dont_send_this', other_param: 'send_this'
+
+ expect(event_data).to include('other_param=send_this')
+ expect(event_data).to include('password=********')
+ end
+ end
end
describe '.authenticate_non_get!' do
@@ -490,11 +563,10 @@ describe API::Helpers 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
- expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
+ expect { authenticate! }.to raise_error /401/
end
end
@@ -502,35 +574,12 @@ describe API::Helpers do
let(:user) { build(:user) }
before do
- 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)
+ expect_any_instance_of(self.class).to receive(: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/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 1583d1c2435..972e57bc373 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -22,7 +22,8 @@ describe API::Issues, :mailer do
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
- updated_at: 3.hours.ago
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
@@ -738,6 +739,7 @@ describe API::Issues, :mailer do
expect(json_response['title']).to eq(issue.title)
expect(json_response['description']).to eq(issue.description)
expect(json_response['state']).to eq(issue.state)
+ expect(json_response['closed_at']).to be_falsy
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
@@ -748,6 +750,13 @@ describe API::Issues, :mailer do
expect(json_response['confidential']).to be_falsy
end
+ it "exposes the 'closed_at' attribute" do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['closed_at']).to be_present
+ end
+
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 21d2c9644fb..5e66e1607ba 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe API::MergeRequests do
+ include ProjectForksHelper
+
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -28,10 +30,29 @@ describe API::MergeRequests do
describe 'GET /merge_requests' do
context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/merge_requests')
+ it 'returns an array of all merge requests' do
+ get api('/merge_requests', user), scope: 'all'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns authentication error without any scope" do
+ get api("/merge_requests")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is assigned-to-me" do
+ get api("/merge_requests"), scope: 'assigned-to-me'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is created-by-me" do
+ get api("/merge_requests"), scope: 'created-by-me'
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
end
end
@@ -134,10 +155,18 @@ describe API::MergeRequests do
describe "GET /projects/:id/merge_requests" do
context "when unauthenticated" do
- it "returns authentication error" do
+ it 'returns merge requests for public projects' do
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns 404 for non public projects" do
+ project = create(:project, :private)
+ get api("/projects/#{project.id}/merge_requests")
+
+ expect(response).to have_http_status(404)
end
end
@@ -589,17 +618,17 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:forked_project) { fork_project(project, user2) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
- fork_project.add_reporter(user2)
+ forked_project.add_reporter(user2)
allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
expect(response).to have_gitlab_http_status(201)
@@ -608,10 +637,10 @@ describe API::MergeRequests do
end
it "does not return 422 when source_branch equals target_branch" do
- expect(project.id).not_to eq(fork_project.id)
- expect(fork_project.forked?).to be_truthy
- expect(fork_project.forked_from_project).to eq(project)
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ expect(project.id).not_to eq(forked_project.id)
+ expect(forked_project.forked?).to be_truthy
+ expect(forked_project.forked_from_project).to eq(project)
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
@@ -620,7 +649,7 @@ describe API::MergeRequests do
it 'returns 422 when target project has disabled merge requests' do
project.project_feature.update(merge_requests_access_level: 0)
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test',
target_branch: 'master',
source_branch: 'markdown',
@@ -631,36 +660,26 @@ describe API::MergeRequests do
end
it "returns 400 when source_branch is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
- it 'returns 422 if not a forked project' do
- post api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- target_branch: 'master',
- source_branch: 'markdown',
- author: user,
- target_project_id: fork_project.id
- expect(response).to have_gitlab_http_status(422)
- end
-
it 'returns 422 if targeting a different fork' do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
@@ -671,8 +690,8 @@ describe API::MergeRequests do
end
it "returns 201 when target_branch is specified and for the same project" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id
expect(response).to have_gitlab_http_status(201)
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index f5882c0c74a..fb440fa551c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -302,6 +302,40 @@ describe API::Notes do
expect(private_issue.notes.reload).to be_empty
end
end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), body: 'Hi!' }
+
+ it 'returns 200 status' do
+ subject
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'creates a new note' do
+ expect { subject }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", private_user), body: 'Hi!' }
+
+ it 'returns 403 status' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not create a new note' do
+ expect { subject }.not_to change { Note.count }
+ end
+ end
+ end
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9602584f546..5964244f8c5 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -54,9 +54,9 @@ describe API::Projects do
shared_examples_for 'projects response without N + 1 queries' do
it 'avoids N + 1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
+ control = ActiveRecord::QueryRecorder.new do
get api('/projects', current_user)
- end.count
+ end
if defined?(additional_project)
additional_project
@@ -64,9 +64,12 @@ describe API::Projects do
create(:project, :public)
end
+ # TODO: We're currently querying to detect if a project is a fork
+ # in 2 ways. Lower this back to 8 when `ForkedProjectLink` relation is
+ # removed
expect do
get api('/projects', current_user)
- end.not_to exceed_query_limit(control_count + 8)
+ end.not_to exceed_query_limit(control).with_threshold(9)
end
end
@@ -193,6 +196,7 @@ describe API::Projects do
path path_with_namespace
star_count forks_count
created_at last_activity_at
+ avatar_url
)
get api('/projects?simple=true', user)
@@ -1181,6 +1185,59 @@ describe API::Projects do
end
end
end
+
+ describe 'GET /projects/:id/forks' do
+ let(:private_fork) { create(:project, :private, :empty_repo) }
+ let(:member) { create(:user) }
+ let(:non_member) { create(:user) }
+
+ before do
+ private_fork.add_developer(member)
+ end
+
+ context 'for a forked project' do
+ before do
+ post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin)
+ private_fork.reload
+ expect(private_fork.forked_from_project).not_to be_nil
+ expect(private_fork.forked?).to be_truthy
+ project_fork_source.reload
+ expect(project_fork_source.forks.length).to eq(1)
+ expect(project_fork_source.forks).to include(private_fork)
+ end
+
+ context 'for a user that can access the forks' do
+ it 'returns the forks' do
+ get api("/projects/#{project_fork_source.id}/forks", member)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['name']).to eq(private_fork.name)
+ end
+ end
+
+ context 'for a user that cannot access the forks' do
+ it 'returns an empty array' do
+ get api("/projects/#{project_fork_source.id}/forks", non_member)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(0)
+ end
+ end
+ end
+
+ context 'for a non-forked project' do
+ it 'returns an empty array' do
+ get api("/projects/#{project_fork_source.id}/forks")
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(0)
+ end
+ end
+ end
end
describe "POST /projects/:id/share" do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 25924559952..8247900408b 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -361,6 +361,8 @@ describe API::Runner do
'policy' => 'pull-push' }]
end
+ let(:expected_features) { { 'trace_sections' => true } }
+
it 'picks a job' do
request_job info: { platform: :darwin }
@@ -380,6 +382,7 @@ describe API::Runner do
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
expect(json_response['variables']).to include(*expected_variables)
+ expect(json_response['features']).to eq(expected_features)
end
context 'when job is made for tag' do
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 48d99841385..7e174903918 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -1,10 +1,13 @@
require "spec_helper"
describe API::Services do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let(:user2) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
+ set(:user2) { create(:user) }
+
+ set(:project) do
+ create(:project, creator_id: user.id, namespace: user.namespace)
+ end
Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
@@ -98,8 +101,6 @@ describe API::Services do
end
describe 'POST /projects/:id/services/:slug/trigger' do
- let!(:project) { create(:project) }
-
describe 'Mattermost Service' do
let(:service_name) { 'mattermost_slash_commands' }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 37cb95a16e3..69c8aa4482a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -23,8 +23,7 @@ describe API::Users do
it "returns the user when a valid `username` parameter is passed" do
get api("/users"), username: user.username
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
expect(json_response[0]['id']).to eq(user.id)
expect(json_response[0]['username']).to eq(user.username)
@@ -68,7 +67,7 @@ describe API::Users do
it "renders 200" do
get api("/users", user)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
end
end
@@ -76,7 +75,7 @@ describe API::Users do
it "renders 200" do
get api("/users", admin)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
end
end
end
@@ -84,9 +83,8 @@ describe API::Users do
it "returns an array of users" do
get api("/users", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
user['username'] == username
@@ -99,18 +97,16 @@ describe API::Users do
get api("/users?blocked=true", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
end
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -123,25 +119,26 @@ describe API::Users do
it 'does not reveal the `is_admin` flag of the user' do
get api('/users', user)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'is_admin'
end
end
context "when admin" do
+ context 'when sudo is defined' do
+ it 'does not return 500' do
+ admin_personal_access_token = create(:personal_access_token, user: admin).token
+ get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin)
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+
it "returns an array of users" do
get api("/users", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first.keys).to include 'email'
- expect(json_response.first.keys).to include 'organization'
- expect(json_response.first.keys).to include 'identities'
- expect(json_response.first.keys).to include 'can_create_project'
- expect(json_response.first.keys).to include 'two_factor_enabled'
- expect(json_response.first.keys).to include 'last_sign_in_at'
- expect(json_response.first.keys).to include 'confirmed_at'
- expect(json_response.first.keys).to include 'is_admin'
end
it "returns an array of external users" do
@@ -149,17 +146,15 @@ describe API::Users do
get api("/users?external=true", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
- 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(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -181,7 +176,7 @@ describe API::Users do
get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
@@ -191,7 +186,7 @@ describe API::Users do
get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(0)
end
@@ -200,7 +195,7 @@ describe API::Users do
get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
@@ -211,22 +206,22 @@ describe API::Users do
it "returns a user by id" do
get api("/users/#{user.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basic')
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(response).to have_http_status(200)
- expect(json_response['is_admin']).to be_nil
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include 'is_admin'
end
context 'when authenticated as admin' do
it 'includes the `is_admin` field' do
get api("/users/#{user.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['is_admin']).to be(false)
end
end
@@ -235,7 +230,7 @@ describe API::Users do
it "returns a user by id" do
get api("/users/#{user.id}")
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response['username']).to eq(user.username)
end
@@ -251,6 +246,7 @@ describe API::Users do
it "returns a 404 error if user id not found" do
get api("/users/9999", user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1909,4 +1905,8 @@ describe API::Users do
expect(impersonation_token.reload.revoked).to be_truthy
end
end
+
+ include_examples 'custom attributes endpoints', 'users' do
+ let(:attributable) { user }
+ end
end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index 681e8e04295..36d793f505d 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::V3::AwardEmoji do
- let(:user) { create(:user) }
- let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
- let!(:note) { create(:note, project: project, noteable: issue) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ set(:note) { create(:note, project: project, noteable: issue) }
before { project.team << [user, :master] }
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index b86aab2ec70..ea2627142bf 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -1,28 +1,28 @@
require 'spec_helper'
describe API::V3::Boards do
- let(:user) { create(:user) }
- let(:guest) { create(:user) }
- let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ set(:user) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
- let!(:dev_label) do
+ set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
- let!(:test_label) do
+ set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
- let!(:dev_list) do
+ set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
- let!(:test_list) do
+ set(:test_list) do
create(:list, label: test_label, position: 2)
end
- let!(:board) do
+ set(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
@@ -98,8 +98,11 @@ describe API::V3::Boards do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ set(:owner) { create(:user) }
+
+ before do
+ project.update(namespace: owner.namespace)
+ end
it "deletes the list if an admin requests it" do
delete v3_api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index c88f7788697..9cd11a67712 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
require 'mime/types'
describe API::V3::Branches do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:project) { create(:project, :repository, creator: user) }
+ set(:master) { create(:project_member, :master, user: user, project: project) }
+ set(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
index 948cd78c177..d04b1c72004 100644
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
describe API::V3::BroadcastMessages do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
describe 'DELETE /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
+ set(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
delete v3_api("/broadcast_messages/#{message.id}"),
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 37710aedbb1..3546b5b85c1 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
describe API::V3::Builds do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:api_user) { user }
- let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
- let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- let(:reporter) { create(:project_member, :reporter, project: project) }
- let(:guest) { create(:project_member, :guest, project: project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ set(:project) { create(:project, :repository, creator: user, public_builds: false) }
+ set(:developer) { create(:project_member, :developer, user: user, project: project) }
+ set(:reporter) { create(:project_member, :reporter, project: project) }
+ set(:guest) { create(:project_member, :guest, project: project) }
+ set(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 9a0e6647ebf..86768d7397a 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::V3::Issues, :mailer do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:author) { create(:author) }
- let(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ set(:admin) { create(:user, :admin) }
let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
@@ -822,7 +822,8 @@ describe API::V3::Issues, :mailer do
end
context 'resolving issues in a merge request' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) }
+ let(:discussion) { diff_note_on_merge_request.to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
@@ -1169,7 +1170,7 @@ describe API::V3::Issues, :mailer do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
+ set(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 86f38dd4ec1..df73c731c96 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe API::MergeRequests do
+ include ProjectForksHelper
+
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -312,17 +314,17 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:forked_project) { fork_project(project, user2) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
- fork_project.add_reporter(user2)
+ forked_project.add_reporter(user2)
allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
expect(response).to have_gitlab_http_status(201)
@@ -331,10 +333,10 @@ describe API::MergeRequests do
end
it "does not return 422 when source_branch equals target_branch" do
- expect(project.id).not_to eq(fork_project.id)
- expect(fork_project.forked?).to be_truthy
- expect(fork_project.forked_from_project).to eq(project)
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ expect(project.id).not_to eq(forked_project.id)
+ expect(forked_project.forked?).to be_truthy
+ expect(forked_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
@@ -343,7 +345,7 @@ describe API::MergeRequests do
it "returns 422 when target project has disabled merge requests" do
project.project_feature.update(merge_requests_access_level: 0)
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test',
target_branch: "master",
source_branch: 'markdown',
@@ -354,36 +356,26 @@ describe API::MergeRequests do
end
it "returns 400 when source_branch is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
- it 'returns 422 if not a forked project' do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- target_branch: 'master',
- source_branch: 'markdown',
- author: user,
- target_project_id: fork_project.id
- expect(response).to have_gitlab_http_status(422)
- end
-
it 'returns 422 if targeting a different fork' do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
@@ -394,8 +386,8 @@ describe API::MergeRequests do
end
it "returns 201 when target_branch is specified and for the same project" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id
expect(response).to have_gitlab_http_status(201)
end
end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index cae2c3118da..e5282c3311f 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -89,6 +89,7 @@ describe API::V3::Projects do
path path_with_namespace
star_count forks_count
created_at last_activity_at
+ avatar_url
)
get v3_api('/projects?simple=true', user)
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 8d79ea3dd40..41bf43a9bce 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -49,6 +49,10 @@ describe JwtController do
let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
subject! { get '/jwt/auth', parameters, headers }
it 'authenticates correctly' do
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 00f45e5f702..980e9a38b3c 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Git LFS API and storage' do
include WorkhorseHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
@@ -841,6 +842,34 @@ describe 'Git LFS API and storage' do
end
end
+ describe 'when handling lfs batch request on a read-only GitLab instance' do
+ let(:authorization) { authorize_user }
+ let(:project) { create(:project) }
+ let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
+ let(:body) do
+ { 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] }
+ end
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ project.team << [user, :master]
+ enable_lfs
+ end
+
+ it 'responds with a 200 message on download' do
+ post_lfs_json path, body.merge('operation' => 'download'), headers
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'responds with a 403 message on upload' do
+ post_lfs_json path, body.merge('operation' => 'upload'), headers
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
+ end
+ end
+
describe 'when pushing a lfs object' do
before do
enable_lfs
@@ -1239,11 +1268,6 @@ describe 'Git LFS API and storage' do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end
- def fork_project(project, user, object = nil)
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(project, user, {}).execute
- end
-
def post_lfs_json(url, body = nil, headers = nil)
post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json'))
end
diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
new file mode 100644
index 00000000000..278662d32ea
--- /dev/null
+++ b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata'
+
+describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ let(:source_file) { 'foo_spec.rb' }
+
+ # Override `CopHelper#inspect_source` to always appear to be in a spec file,
+ # so that our RSpec-only cop actually runs
+ def inspect_source(*args)
+ super(*args, source_file)
+ end
+
+ shared_examples 'examples with include syntax' do |title|
+ it "flags violation for #{title} examples that uses verbose include syntax" do
+ inspect_source(cop, "#{title} 'Test', js: true do; end")
+
+ expect(cop.offenses.size).to eq(1)
+ offense = cop.offenses.first
+
+ expect(offense.line).to eq(1)
+ expect(cop.highlights).to eq(["#{title} 'Test', js: true"])
+ expect(offense.message).to eq('Use `:js` instead of `js: true`.')
+ end
+
+ it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do
+ inspect_source(cop, "#{title} 'Test', :js do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} examples that uses flag: symbol" do
+ inspect_source(cop, "#{title} 'Test', flag: :symbol do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do
+ autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file)
+
+ expect(autocorrected).to eql("#{title} 'Test', :js do; end")
+ end
+ end
+
+ %w(describe context feature example_group it specify example scenario its).each do |example|
+ it_behaves_like 'examples with include syntax', example
+ end
+end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 5b7822d5d8e..f6bd6e9ede4 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BuildDetailsEntity do
+ include ProjectForksHelper
+
set(:user) { create(:admin) }
it 'inherits from JobEntity' do
@@ -56,18 +58,16 @@ describe BuildDetailsEntity do
end
context 'when merge request is from a fork' do
- let(:fork_project) do
- create(:project, forked_from_project: project)
- end
+ let(:forked_project) { fork_project(project) }
- let(:pipeline) { create(:ci_pipeline, project: fork_project) }
+ let(:pipeline) { create(:ci_pipeline, project: forked_project) }
before do
allow(build).to receive(:merge_request).and_return(merge_request)
end
let(:merge_request) do
- create(:merge_request, source_project: fork_project,
+ create(:merge_request, source_project: forked_project,
target_project: project,
source_branch: build.ref)
end
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 01e2cfed6f8..9673b11c2a2 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("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
end
end
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
new file mode 100644
index 00000000000..2c7f49974f1
--- /dev/null
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ClusterEntity do
+ set(:cluster) { create(:gcp_cluster, :errored) }
+ let(:request) { double('request') }
+
+ let(:entity) do
+ described_class.new(cluster)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains status' do
+ expect(subject[:status]).to eq(:errored)
+ end
+
+ it 'contains status reason' do
+ expect(subject[:status_reason]).to eq('general error')
+ end
+ end
+end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
new file mode 100644
index 00000000000..1ac6784d28f
--- /dev/null
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe ClusterSerializer do
+ let(:serializer) do
+ described_class.new
+ end
+
+ describe '#represent_status' do
+ subject { serializer.represent_status(resource) }
+
+ context 'when represents only status' do
+ let(:resource) { create(:gcp_cluster, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
new file mode 100644
index 00000000000..c589cd18f77
--- /dev/null
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe ContainerRepositoryEntity do
+ let(:entity) do
+ described_class.new(repository, request: request)
+ end
+
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:repository) { create(:container_repository, project: project) }
+
+ let(:request) { double('request') }
+
+ subject { entity.as_json }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(request).to receive(:project).and_return(project)
+ allow(request).to receive(:current_user).and_return(user)
+ end
+
+ it 'exposes required informations' do
+ expect(subject).to include(:id, :path, :location, :tags_path)
+ end
+
+ context 'when user can manage repositories' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'exposes destroy_path' do
+ expect(subject).to include(:destroy_path)
+ end
+ end
+
+ context 'when user cannot manage repositories' do
+ it 'does not expose destroy_path' do
+ expect(subject).not_to include(:destroy_path)
+ end
+ end
+end
diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb
new file mode 100644
index 00000000000..6dcc5204516
--- /dev/null
+++ b/spec/serializers/container_tag_entity_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ContainerTagEntity do
+ let(:entity) do
+ described_class.new(tag, request: request)
+ end
+
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:repository) { create(:container_repository, name: 'image', project: project) }
+
+ let(:request) { double('request') }
+ let(:tag) { repository.tag('test') }
+
+ subject { entity.as_json }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: /image/, tags: %w[test])
+ allow(request).to receive(:project).and_return(project)
+ allow(request).to receive(:current_user).and_return(user)
+ end
+
+ it 'exposes required informations' do
+ expect(subject).to include(:name, :location, :revision, :total_size, :created_at)
+ end
+
+ context 'when user can manage repositories' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'exposes destroy_path' do
+ expect(subject).to include(:destroy_path)
+ end
+ end
+
+ context 'when user cannot manage repositories' do
+ it 'does not expose destroy_path' do
+ expect(subject).not_to include(:destroy_path)
+ end
+ end
+end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 979d9921941..8f32c5639a1 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -16,6 +16,10 @@ describe EnvironmentEntity do
expect(subject).to include(:id, :name, :state, :environment_path)
end
+ it 'exposes folder path' do
+ expect(subject).to include(:folder_path)
+ end
+
context 'metrics disabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(false)
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index a2fd5b7daae..4aeb593da44 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -11,16 +11,6 @@ describe MergeRequestEntity do
described_class.new(resource, request: request).as_json
end
- it 'includes author' do
- req = double('request')
-
- author_payload = UserEntity
- .represent(resource.author, request: req)
- .as_json
-
- expect(subject[:author]).to eq(author_payload)
- end
-
it 'includes pipeline' do
req = double('request', current_user: user)
pipeline = build_stubbed(:ci_pipeline)
@@ -47,7 +37,8 @@ describe MergeRequestEntity do
:cancel_merge_when_pipeline_succeeds_path,
:create_issue_to_resolve_discussions_path,
:source_branch_path, :target_branch_commits_path,
- :target_branch_tree_path, :commits_count, :merge_ongoing)
+ :target_branch_tree_path, :commits_count, :merge_ongoing,
+ :ff_only_enabled)
end
it 'has email_patches_path' do
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index f8df461bc81..248552d1858 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -108,5 +108,18 @@ describe PipelineEntity do
expect(subject[:ref][:path]).to be_nil
end
end
+
+ context 'when pipeline has a failure reason set' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ pipeline.drop!(:config_error)
+ end
+
+ it 'has a correct failure reason' do
+ expect(subject[:failure_reason])
+ .to eq 'CI/CD YAML configuration error!'
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 2de8daba6b5..8fc1ceedc34 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -103,9 +103,15 @@ describe PipelineSerializer do
let(:project) { create(:project) }
before do
- Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
- create_pipeline(status)
+ # Since RequestStore.active? is true we have to allow the
+ # gitaly calls in this block
+ # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
+ create_pipeline(status)
+ end
end
+ Gitlab::GitalyClient.reset_counts
end
shared_examples 'no N+1 queries' do
@@ -162,7 +168,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("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/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 3964b998084..16431ed4188 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,12 +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')
+ expect(subject[:favicon]).to match_asset_path('/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')
+ expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico')
end
end
end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
new file mode 100644
index 00000000000..6e7398fbffa
--- /dev/null
+++ b/spec/services/ci/create_cluster_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Ci::CreateClusterService do
+ describe '#execute' do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when correct params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 1
+ }
+ end
+
+ it 'creates a cluster object' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(1)
+ expect(result.gcp_project_id).to eq('gcp-project')
+ expect(result.gcp_cluster_name).to eq('test-cluster')
+ expect(result.gcp_cluster_zone).to eq('us-central1-a')
+ expect(result.gcp_cluster_size).to eq(1)
+ expect(result.gcp_token).to eq(access_token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 'ABC'
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4c2ff08039c..08847183bf4 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::CreatePipelineService do
+ include ProjectForksHelper
+
set(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
let(:ref_name) { 'refs/heads/master' }
@@ -82,13 +84,9 @@ describe Ci::CreatePipelineService do
end
context 'when merge request target project is different from source project' do
+ let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- let!(:forked_project_link) do
- create(:forked_project_link, forked_to_project: project,
- forked_from_project: target_project)
- end
-
it 'updates head pipeline for merge request' do
merge_request = create(:merge_request, source_branch: 'master',
target_branch: "branch_1",
@@ -133,6 +131,26 @@ describe Ci::CreatePipelineService do
expect(merge_request.reload.head_pipeline).to eq head_pipeline
end
end
+
+ context 'when pipeline has been skipped' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:git_commit_message)
+ .and_return('some commit [ci skip]')
+ end
+
+ it 'updates merge request head pipeline' do
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: 'feature',
+ source_project: project)
+
+ head_pipeline = execute_service
+
+ expect(head_pipeline).to be_skipped
+ expect(head_pipeline).to be_persisted
+ expect(merge_request.reload.head_pipeline).to eq head_pipeline
+ end
+ end
end
context 'auto-cancel enabled' do
@@ -481,104 +499,4 @@ describe Ci::CreatePipelineService do
end
end
end
-
- describe '#allowed_to_create?' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:ref) { 'master' }
-
- subject do
- described_class.new(project, user, ref: ref)
- .send(:allowed_to_create?)
- end
-
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- end
-
- it { is_expected.to be_truthy }
-
- context 'when the branch is protected' do
- let!(:protected_branch) do
- create(:protected_branch, project: project, name: ref)
- end
-
- it { is_expected.to be_falsey }
-
- context 'when developers are allowed to merge' do
- let!(:protected_branch) do
- create(:protected_branch,
- :developers_can_merge,
- project: project,
- name: ref)
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when the tag is protected' do
- let(:ref) { 'v1.0.0' }
-
- let!(:protected_tag) do
- create(:protected_tag, project: project, name: ref)
- end
-
- it { is_expected.to be_falsey }
-
- context 'when developers are allowed to create the tag' do
- let!(:protected_tag) do
- create(:protected_tag,
- :developers_can_create,
- project: project,
- name: ref)
- end
-
- it { is_expected.to be_truthy }
- end
- end
- end
-
- context 'when user is a master' do
- before do
- project.add_master(user)
- end
-
- it { is_expected.to be_truthy }
-
- context 'when the branch is protected' do
- let!(:protected_branch) do
- create(:protected_branch, project: project, name: ref)
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when the tag is protected' do
- let(:ref) { 'v1.0.0' }
-
- let!(:protected_tag) do
- create(:protected_tag, project: project, name: ref)
- end
-
- it { is_expected.to be_truthy }
-
- context 'when no one can create the tag' do
- let!(:protected_tag) do
- create(:protected_tag,
- :no_one_can_create,
- project: project,
- name: ref)
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- context 'when owner cannot create pipeline' do
- it { is_expected.to be_falsey }
- end
- end
end
diff --git a/spec/services/ci/extract_sections_from_build_trace_service_spec.rb b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
new file mode 100644
index 00000000000..28f2fa7903a
--- /dev/null
+++ b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Ci::ExtractSectionsFromBuildTraceService, '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, project: project) }
+
+ subject { described_class.new(project, user) }
+
+ shared_examples 'build trace has sections markers' do
+ before do
+ build.trace.set(File.read(expand_fixture_path('trace/trace_with_sections')))
+ end
+
+ it 'saves the correct extracted sections' do
+ expect(build.trace_sections).to be_empty
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).not_to be_empty
+ end
+
+ it "fails if trace_sections isn't empty" do
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).not_to be_empty
+
+ expect(subject.execute(build)).to be(false)
+ expect(build.trace_sections).not_to be_empty
+ end
+ end
+
+ shared_examples 'build trace has no sections markers' do
+ before do
+ build.trace.set('no markerts')
+ end
+
+ it 'extracts no sections' do
+ expect(build.trace_sections).to be_empty
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).to be_empty
+ end
+ end
+
+ context 'when the build has no user' do
+ it_behaves_like 'build trace has sections markers'
+ it_behaves_like 'build trace has no sections markers'
+ end
+
+ context 'when the build has a valid user' do
+ before do
+ build.user = user
+ end
+
+ it_behaves_like 'build trace has sections markers'
+ it_behaves_like 'build trace has no sections markers'
+ end
+end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
new file mode 100644
index 00000000000..7792979c5cb
--- /dev/null
+++ b/spec/services/ci/fetch_gcp_operation_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'google/apis'
+
+describe Ci::FetchGcpOperationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ context 'when suceeded' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_return(operation)
+ end
+
+ it 'fetch the gcp operaion' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .to yield_with_args(operation)
+ end
+ end
+
+ context 'when raises an error' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .not_to yield_with_args
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
new file mode 100644
index 00000000000..1d05c9671a9
--- /dev/null
+++ b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Ci::FetchKubernetesTokenService do
+ describe '#execute' do
+ subject { described_class.new(api_url, ca_pem, username, password).execute }
+
+ let(:api_url) { 'http://111.111.111.111' }
+ let(:ca_pem) { '' }
+ let(:username) { 'admin' }
+ let(:password) { 'xxx' }
+
+ context 'when params correct' do
+ let(:token) { 'xxx.token.xxx' }
+
+ let(:secrets_json) do
+ [
+ {
+ 'metadata': {
+ name: metadata_name
+ },
+ 'data': {
+ 'token': Base64.encode64(token)
+ }
+ }
+ ]
+ end
+
+ before do
+ allow_any_instance_of(Kubeclient::Client)
+ .to receive(:get_secrets).and_return(secrets_json)
+ end
+
+ context 'when default-token exists' do
+ let(:metadata_name) { 'default-token-123' }
+
+ it { is_expected.to eq(token) }
+ end
+
+ context 'when default-token does not exist' do
+ let(:metadata_name) { 'another-token-123' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when username is nil' do
+ let(:username) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when password is nil' do
+ let(:password) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+ end
+end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
new file mode 100644
index 00000000000..def3709fdb4
--- /dev/null
+++ b/spec/services/ci/finalize_cluster_creation_service_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Ci::FinalizeClusterCreationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:result) { described_class.new.execute(cluster) }
+
+ context 'when suceeded to get cluster from api' do
+ let(:gke_cluster) { double }
+
+ before do
+ allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
+ allow(gke_cluster).to receive(:master_auth).and_return(spy)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
+ end
+
+ context 'when suceeded to get kubernetes token' do
+ let(:kubernetes_token) { 'abc' }
+
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(kubernetes_token)
+ end
+
+ it 'executes integration cluster' do
+ expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when failed to get kubernetes token' do
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(nil)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when failed to get cluster from api' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
new file mode 100644
index 00000000000..3a79c205bd1
--- /dev/null
+++ b/spec/services/ci/integrate_cluster_service_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Ci::IntegrateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
+ let(:endpoint) { '123.123.123.123' }
+ let(:ca_cert) { 'ca_cert_xxx' }
+ let(:token) { 'token_xxx' }
+ let(:username) { 'username_xxx' }
+ let(:password) { 'password_xxx' }
+
+ before do
+ described_class
+ .new.execute(cluster, endpoint, ca_cert, token, username, password)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ it 'creates a cluster object' do
+ expect(cluster.endpoint).to eq(endpoint)
+ expect(cluster.ca_cert).to eq(ca_cert)
+ expect(cluster.kubernetes_token).to eq(token)
+ expect(cluster.username).to eq(username)
+ expect(cluster.password).to eq(password)
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:endpoint) { nil }
+
+ it 'sets an error to cluster object' do
+ expect(cluster).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 9a6875e448c..f4ff818c479 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -34,6 +34,8 @@ describe Ci::PipelineTriggerService do
expect(result[:pipeline].ref).to eq('master')
expect(result[:pipeline].project).to eq(project)
expect(result[:pipeline].user).to eq(trigger.owner)
+ expect(result[:pipeline].trigger_requests.to_a)
+ .to eq(result[:pipeline].builds.map(&:trigger_request).uniq)
expect(result[:status]).to eq(:success)
end
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
new file mode 100644
index 00000000000..5ce5c788314
--- /dev/null
+++ b/spec/services/ci/provision_cluster_service_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Ci::ProvisionClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { spy }
+
+ shared_examples 'error' do
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ allow(operation).to receive(:status).and_return('RUNNING')
+ end
+
+ context 'when suceeded to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ context 'when cluster status is scheduled' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when cluster status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return(nil)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is others' do
+ before do
+ allow(operation).to receive(:status).and_return('others')
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to request provision' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_raise(error)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 7f29fac9eef..1005156d18e 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
describe Ci::RetryBuildService do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:service) do
@@ -19,7 +20,7 @@ describe Ci::RetryBuildService do
erased_at auto_canceled_by].freeze
IGNORE_ACCESSORS =
- %i[type lock_version target_url base_tags
+ %i[type lock_version target_url base_tags trace_sections
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
@@ -38,7 +39,7 @@ describe Ci::RetryBuildService do
:queued, :coverage, :tags, :allowed_to_fail, :on_tag,
:triggered, :trace, :teardown_environment,
description: 'my-job', stage: 'test', pipeline: pipeline,
- auto_canceled_by: create(:ci_empty_pipeline)) do |build|
+ auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build|
##
# TODO, workaround for FactoryGirl limitation when having both
# stage (text) and stage_id (integer) columns in the table.
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
new file mode 100644
index 00000000000..a289385b88f
--- /dev/null
+++ b/spec/services/ci/update_cluster_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Ci::UpdateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
+
+ before do
+ described_class.new(cluster.project, cluster.user, params).execute(cluster)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { 'enabled' => 'true' } }
+
+ it 'enables cluster and overwrite kubernetes service' do
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(cluster.kubernetes_token)
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { 'enabled' => 'false' } }
+
+ it 'disables cluster and kubernetes service' do
+ expect(cluster.enabled).to be_falsy
+ expect(cluster.service.active).to be_falsy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 03c682ae0d7..5a9eb359ee1 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe DeleteMergedBranchesService do
+ include ProjectForksHelper
+
subject(:service) { described_class.new(project, project.owner) }
let(:project) { create(:project, :repository) }
@@ -50,9 +52,9 @@ describe DeleteMergedBranchesService do
context 'open merge requests' do
it 'does not delete branches from open merge requests' do
- fork_link = create(:forked_project_link, forked_from_project: project)
+ forked_project = fork_project(project)
create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master')
- create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
+ create(:merge_request, :opened, source_project: forked_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
service.execute
diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb
new file mode 100644
index 00000000000..7a604c0cadd
--- /dev/null
+++ b/spec/services/deploy_keys/create_service_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe DeployKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:deploy_key) }
+
+ subject { described_class.new(user, params) }
+
+ it "creates a deploy key" do
+ expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1)
+ end
+end
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 82b156f5ebe..2b84206318f 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do
change_position = discussion.change_position
expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
- expect(change_position.old_line).to eq(9)
- expect(change_position.new_line).to be_nil
+ expect(change_position.formatter.old_line).to eq(9)
+ expect(change_position.formatter.new_line).to be_nil
end
it 'creates a system discussion' do
@@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do
expect(discussion.original_position).to eq(old_position)
expect(discussion.position).not_to eq(old_position)
- expect(discussion.position.new_line).to eq(22)
+ expect(discussion.position.formatter.new_line).to eq(22)
end
context 'when the resolve_outdated_diff_discussions setting is set' do
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
new file mode 100644
index 00000000000..2b2c31e2521
--- /dev/null
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Emails::ConfirmService do
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user) }
+
+ describe '#execute' do
+ it 'sends a confirmation email again' do
+ email = user.emails.create(email: 'new@email.com')
+ mail = service.execute(email)
+ expect(mail.subject).to eq('Confirmation instructions')
+ end
+ end
+end
diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb
index 641d5538de8..54692c88623 100644
--- a/spec/services/emails/create_service_spec.rb
+++ b/spec/services/emails/create_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Emails::CreateService do
let(:user) { create(:user) }
- let(:opts) { { email: 'new@email.com' } }
+ let(:opts) { { email: 'new@email.com', user: user } }
subject(:service) { described_class.new(user, opts) }
@@ -12,6 +12,11 @@ describe Emails::CreateService do
expect(Email.where(opts)).not_to be_empty
end
+ it 'creates an email with additional attributes' do
+ expect { service.execute(confirmation_token: 'abc') }.to change { Email.count }.by(1)
+ expect(Email.where(opts).first.confirmation_token).to eq 'abc'
+ end
+
it 'has the right user association' do
service.execute
diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb
index 1f4294dd905..c3204fac3df 100644
--- a/spec/services/emails/destroy_service_spec.rb
+++ b/spec/services/emails/destroy_service_spec.rb
@@ -4,11 +4,11 @@ describe Emails::DestroyService do
let!(:user) { create(:user) }
let!(:email) { create(:email, user: user) }
- subject(:service) { described_class.new(user, email: email.email) }
+ subject(:service) { described_class.new(user, user: user) }
describe '#execute' do
it 'removes an email' do
- expect { service.execute }.to change { user.emails.count }.by(-1)
+ expect { service.execute(email) }.to change { user.emails.count }.by(-1)
end
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 02d7ddeb86b..13395a7cac3 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -149,6 +149,14 @@ describe EventCreateService do
.to change { user_activity(user) }
end
+ it 'caches the last push event for the user' do
+ expect_any_instance_of(Users::LastPushEventService)
+ .to receive(:cache_last_push_event)
+ .with(an_instance_of(PushEvent))
+
+ service.push(project, user, push_data)
+ end
+
it 'does not create any event data when an error is raised' do
payload_service = double(:service)
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
new file mode 100644
index 00000000000..1cd2625531e
--- /dev/null
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe GpgKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:gpg_key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a gpg key' do
+ expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1)
+ end
+
+ context 'when the public key contains subkeys' do
+ let(:params) { attributes_for(:gpg_key_with_subkeys) }
+
+ it 'generates the gpg subkeys' do
+ gpg_key = subject.execute
+
+ expect(gpg_key.subkeys.count).to eq(2)
+ end
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 171f70c32a8..5c27e8fd561 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -42,7 +42,7 @@ describe Issues::CloseService do
service.execute(issue)
end
- it 'refreshes the number of open issues' do
+ it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { service.execute(issue) }
.to change { project.open_issues_count }.from(1).to(0)
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index cc3d648c340..d86da244520 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -35,7 +35,7 @@ describe Issues::CreateService do
expect(issue.due_date).to eq Date.tomorrow
end
- it 'refreshes the number of open issues' do
+ it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { issue }.to change { project.open_issues_count }.from(0).to(1)
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 15a50b85f19..f07b81e842a 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -48,7 +48,8 @@ describe Issues::UpdateService, :mailer do
assignee_ids: [user2.id],
state_event: 'close',
label_ids: [label.id],
- due_date: Date.tomorrow
+ due_date: Date.tomorrow,
+ discussion_locked: true
}
end
@@ -62,6 +63,14 @@ describe Issues::UpdateService, :mailer do
expect(issue).to be_closed
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
+ expect(issue.discussion_locked).to be_truthy
+ end
+
+ it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
+ issue # make sure the issue is created first so our counts are correct.
+
+ expect { update_issue(confidential: true) }
+ .to change { project.open_issues_count }.from(1).to(0)
end
it 'updates open issue counter for assignees when issue is reassigned' do
@@ -103,6 +112,7 @@ describe Issues::UpdateService, :mailer do
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
+ expect(issue.discussion_locked).to be_falsey
end
end
@@ -141,6 +151,13 @@ describe Issues::UpdateService, :mailer do
expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
+
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this issue')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this issue'
+ end
end
end
@@ -250,6 +267,30 @@ describe Issues::UpdateService, :mailer do
end
end
+ context 'when a new assignee added' do
+ subject { update_issue(assignees: issue.assignees + [user2]) }
+
+ it 'creates only 1 new todo' do
+ expect { subject }.to change { Todo.count }.by(1)
+ end
+
+ it 'creates a todo for new assignee' do
+ subject
+
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: issue.id,
+ target_type: issue.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq(1)
+ end
+ end
+
context 'when the milestone change' do
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb
new file mode 100644
index 00000000000..bcb436c1e46
--- /dev/null
+++ b/spec/services/keys/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Keys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a key' do
+ expect { subject.execute }.to change { user.keys.where(params).count }.by(1)
+ end
+end
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
new file mode 100644
index 00000000000..bb0fb6acf39
--- /dev/null
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Keys::LastUsedService do
+ describe '#execute', :clean_gitlab_redis_shared_state do
+ it 'updates the key when it has not been used recently' do
+ key = create(:key, last_used_at: 1.year.ago)
+ time = Time.zone.now
+
+ Timecop.freeze(time) { described_class.new(key).execute }
+
+ expect(key.last_used_at).to eq(time)
+ end
+
+ it 'does not update the key when it has been used recently' do
+ time = 1.minute.ago
+ key = create(:key, last_used_at: time)
+
+ described_class.new(key).execute
+
+ expect(key.last_used_at).to eq(time)
+ end
+
+ it 'does not update the updated_at field' do
+ # Since a lot of these updates could happen in parallel for different keys
+ # we want these updates to be as lightweight as possible, hence we want to
+ # make sure we _only_ update last_used_at and not always updated_at.
+ key = create(:key, last_used_at: 1.year.ago)
+
+ expect { described_class.new(key).execute }.not_to change { key.updated_at }
+ end
+ end
+
+ describe '#update?', :clean_gitlab_redis_shared_state do
+ it 'returns true when no last used timestamp is present' do
+ key = build(:key, last_used_at: nil)
+ service = described_class.new(key)
+
+ expect(service.update?).to eq(true)
+ end
+
+ it 'returns true when the key needs to be updated' do
+ key = build(:key, last_used_at: 1.year.ago)
+ service = described_class.new(key)
+
+ expect(service.update?).to eq(true)
+ end
+
+ it 'returns false when a lease has already been obtained' do
+ key = build(:key, last_used_at: 1.year.ago)
+ service = described_class.new(key)
+
+ expect(service.update?).to eq(true)
+ expect(service.update?).to eq(false)
+ end
+
+ it 'returns false when the key does not yet need to be updated' do
+ key = build(:key, last_used_at: 1.minute.ago)
+ service = described_class.new(key)
+
+ expect(service.update?).to eq(false)
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 7e65369762c..b3886987316 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -52,7 +52,7 @@ describe MergeRequests::CloseService do
end
end
- it 'refreshes the number of open merge requests for a valid MR' do
+ it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
service = described_class.new(project, user, {})
expect { service.execute(merge_request) }
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 6f49a65d795..9c9b0c4c4a1 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -1,14 +1,16 @@
require 'spec_helper'
describe MergeRequests::Conflicts::ResolveService do
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- let(:fork_project) do
- create(:forked_project_with_submodules) do |fork_project|
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- end
+ let(:project) { create(:project, :public, :repository) }
+
+ let(:forked_project) do
+ forked_project = fork_project(project, user)
+ TestEnv.copy_repo(forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
+ forked_project
end
let(:merge_request) do
@@ -19,7 +21,7 @@ describe MergeRequests::Conflicts::ResolveService do
let(:merge_request_from_fork) do
create(:merge_request,
- source_branch: 'conflict-resolvable-fork', source_project: fork_project,
+ source_branch: 'conflict-resolvable-fork', source_project: forked_project,
target_branch: 'conflict-start', target_project: project)
end
@@ -114,7 +116,7 @@ describe MergeRequests::Conflicts::ResolveService do
end
it 'gets conflicts from the source project' do
- expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original
+ expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
expect(project.repository.rugged).not_to receive(:merge_commits)
resolve_conflicts
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index d6409c0d625..a047f891ab2 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -37,7 +37,7 @@ describe MergeRequests::CreateService do
expect(service).to have_received(:execute_hooks).with(merge_request)
end
- it 'refreshes the number of open merge requests' do
+ it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do
expect { service.execute }
.to change { project.open_merge_requests_count }.from(0).to(1)
end
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
new file mode 100644
index 00000000000..aaabf3ed2b0
--- /dev/null
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe MergeRequests::FfMergeService do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'flatten-dir',
+ target_branch: 'improve/awesome',
+ assignee: user2)
+ end
+ let(:project) { merge_request.project }
+
+ before do
+ project.team << [user, :master]
+ project.team << [user2, :developer]
+ end
+
+ describe '#execute' do
+ context 'valid params' do
+ let(:service) { described_class.new(project, user, {}) }
+
+ before do
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ it "does not create merge commit" do
+ source_branch_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
+ target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
+ expect(source_branch_sha).to eq(target_branch_sha)
+ end
+
+ it { expect(merge_request).to be_valid }
+ it { expect(merge_request).to be_merged }
+
+ it 'sends email to user2 about merge of new merge_request' do
+ email = ActionMailer::Base.deliveries.last
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(merge_request.title)
+ end
+
+ it 'creates system note about merge_request merge' do
+ note = merge_request.notes.last
+ expect(note.note).to include 'merged'
+ end
+ end
+
+ context "error handling" do
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ allow(Rails.logger).to receive(:error)
+ end
+
+ it 'logs and saves error if there is an exception' do
+ error_message = 'error message'
+
+ allow(service).to receive(:repository).and_raise("error message")
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+
+ it 'logs and saves error if there is an PreReceiveError exception' do
+ error_message = 'error message'
+
+ allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message)
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 25599dea19f..274624aa8bb 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe MergeRequests::GetUrlsService do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
let(:source_branch) { "merge-test" }
@@ -85,7 +87,7 @@ describe MergeRequests::GetUrlsService do
context 'pushing to existing branch from forked project' do
let(:user) { create(:user) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let!(:forked_project) { fork_project(project, user, repository: true) }
let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
let(:changes) { existing_branch_changes }
# Source project is now the forked one
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index b60136064b7..80213d093f1 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -13,20 +13,21 @@ describe MergeRequests::MergeService do
describe '#execute' do
context 'MergeRequest#merge_jid' do
+ let(:service) do
+ described_class.new(project, user, commit_message: 'Awesome message')
+ end
+
before do
merge_request.update_column(:merge_jid, 'hash-123')
end
it 'is cleaned when no error is raised' do
- service = described_class.new(project, user, commit_message: 'Awesome message')
-
service.execute(merge_request)
expect(merge_request.reload.merge_jid).to be_nil
end
it 'is cleaned when expected error is raised' do
- service = described_class.new(project, user, commit_message: 'Awesome message')
allow(service).to receive(:commit).and_raise(described_class::MergeError)
service.execute(merge_request)
@@ -34,6 +35,22 @@ describe MergeRequests::MergeService do
expect(merge_request.reload.merge_jid).to be_nil
end
+ it 'is cleaned when merge request is not mergeable' do
+ allow(merge_request).to receive(:mergeable?).and_return(false)
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_jid).to be_nil
+ end
+
+ it 'is cleaned when no source is found' do
+ allow(merge_request).to receive(:diff_head_sha).and_return(nil)
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_jid).to be_nil
+ end
+
it 'is not cleaned when unexpected error is raised' do
service = described_class.new(project, user, commit_message: 'Awesome message')
allow(service).to receive(:commit).and_raise(StandardError)
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index a37cdab8928..d2bd05d921f 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -11,5 +11,16 @@ describe MergeRequests::PostMergeService do
describe '#execute' do
it_behaves_like 'cache counters invalidator'
+
+ it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
+ # Cache the counter before the MR changed state.
+ project.open_merge_requests_count
+ merge_request.update!(state: 'merged')
+
+ service = described_class.new(project, user, {})
+
+ expect { service.execute(merge_request) }
+ .to change { project.open_merge_requests_count }.from(1).to(0)
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2af2485eeed..62dbe362ec8 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequests::RefreshService do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class }
@@ -12,7 +14,8 @@ describe MergeRequests::RefreshService do
group.add_owner(@user)
@project = create(:project, :repository, namespace: group)
- @fork_project = Projects::ForkService.new(@project, @user).execute
+ @fork_project = fork_project(@project, @user, repository: true)
+
@merge_request = create(:merge_request,
source_project: @project,
source_branch: 'master',
@@ -150,9 +153,7 @@ describe MergeRequests::RefreshService do
context 'manual merge of source branch' do
before do
# Merge master -> feature branch
- author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
- commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
@@ -313,8 +314,7 @@ describe MergeRequests::RefreshService do
context 'when the merge request is sourced from a different project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
- forked_project = create(:project, :repository)
- create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+ forked_project = fork_project(@project, @user, repository: true)
merge_request = create(:merge_request,
target_branch: 'master',
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 681feee61d1..b11a1b31f32 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -49,7 +49,8 @@ describe MergeRequests::UpdateService, :mailer do
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
- force_remove_source_branch: '1'
+ force_remove_source_branch: '1',
+ discussion_locked: true
}
end
@@ -73,6 +74,7 @@ describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.labels.first.title).to eq(label.name)
expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ expect(@merge_request.discussion_locked).to be_truthy
end
it 'executes hooks with update action' do
@@ -123,6 +125,13 @@ describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'changed target branch from `master` to `target`'
end
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this issue')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this issue'
+ end
+
context 'when not including source branch removal options' do
before do
opts.delete(:force_remove_source_branch)
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 3e493148b32..b64ca5be8fc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -84,7 +84,6 @@ describe NotificationService, :mailer do
let!(:key) { create(:personal_key, key_options) }
it { expect(notification.new_key(key)).to be_truthy }
- it { should_email(key.user) }
describe 'never emails the ghost user' do
let(:key_options) { { user: User.ghost } }
@@ -106,18 +105,6 @@ describe NotificationService, :mailer do
end
end
- describe 'Email' do
- describe '#new_email' do
- let!(:email) { create(:email) }
-
- it { expect(notification.new_email(email)).to be_truthy }
-
- it 'sends email to email owner' do
- expect { notification.new_email(email) }.to change { ActionMailer::Base.deliveries.size }.by(1)
- end
- end
- end
-
describe 'Notes' do
context 'issue note' do
let(:project) { create(:project, :private) }
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
index 79b01e7620e..cc496501bad 100644
--- a/spec/services/projects/count_service_spec.rb
+++ b/spec/services/projects/count_service_spec.rb
@@ -66,8 +66,8 @@ describe Projects::CountService do
describe '#cache_key' do
it 'returns the cache key as an Array' do
- allow(service).to receive(:cache_key_name).and_return('count_service')
- expect(service.cache_key).to eq(['projects', 1, 'count_service'])
+ allow(service).to receive(:cache_key_name).and_return('foo')
+ expect(service.cache_key).to eq(['projects', 'count_service', described_class::VERSION, 1, 'foo'])
end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 5da634e2fb1..dc89fdebce7 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -76,9 +76,8 @@ describe Projects::CreateService, '#execute' do
context 'wiki_enabled true creates wiki repository directory' do
it do
project = create_project(user, opts)
- path = ProjectWiki.new(project, user).send(:path_to_repo)
- expect(File.exist?(path)).to be_truthy
+ expect(wiki_repo(project).exists?).to be_truthy
end
end
@@ -86,11 +85,15 @@ describe Projects::CreateService, '#execute' do
it do
opts[:wiki_enabled] = false
project = create_project(user, opts)
- path = ProjectWiki.new(project, user).send(:path_to_repo)
- expect(File.exist?(path)).to be_falsey
+ expect(wiki_repo(project).exists?).to be_falsey
end
end
+
+ def wiki_repo(project)
+ relative_path = ProjectWiki.new(project).disk_path + '.git'
+ Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar')
+ end
end
context 'builds_enabled global setting' do
@@ -149,6 +152,9 @@ describe Projects::CreateService, '#execute' do
end
context 'when another repository already exists on disk' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+
let(:opts) do
{
name: 'Existing',
@@ -156,30 +162,59 @@ describe Projects::CreateService, '#execute' do
}
end
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ context 'with legacy storage' do
+ before do
+ gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing")
+ end
- before do
- gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ it 'does not allow to create a project when path matches existing repository on disk' do
+ project = create_project(user, opts)
- it 'does not allow to create project with same path' do
- project = create_project(user, opts)
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
- expect(project).to respond_to(:errors)
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ it 'does not allow to import project when path matches existing repository on disk' do
+ project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' }))
+
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
- it 'does not allow to import a project with the same path' do
- project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' }))
+ context 'with hashed storage' do
+ let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+ let(:hashed_path) { '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ allow(Digest::SHA2).to receive(:hexdigest) { hash }
+ end
- expect(project).to respond_to(:errors)
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ before do
+ gitlab_shell.add_repository(repository_storage, hashed_path)
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, hashed_path)
+ end
+
+ it 'does not allow to create a project when path matches existing repository on disk' do
+ project = create_project(user, opts)
+
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
end
end
@@ -208,6 +243,15 @@ describe Projects::CreateService, '#execute' do
end
end
+ context 'when skip_disk_validation is used' do
+ it 'sets the project attribute' do
+ opts[:skip_disk_validation] = true
+ project = create_project(user, opts)
+
+ expect(project.skip_disk_validation).to be_truthy
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index c867139d1de..c90bad46295 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -212,6 +212,19 @@ describe Projects::DestroyService do
end
end
+ context 'as the root of a fork network' do
+ let!(:fork_network) { create(:fork_network, root_project: project) }
+
+ it 'updates the fork network with the project name' do
+ destroy_project(project, user)
+
+ fork_network.reload
+
+ expect(fork_network.deleted_root_project_name).to eq(project.full_name)
+ expect(fork_network.root_project).to be_nil
+ end
+ end
+
def destroy_project(project, user, params = {})
if async
Projects::DestroyService.new(project, user, params).async_execute
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index a6e0364d44c..53862283a27 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::ForkService do
+ include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new }
describe 'fork by user' do
@@ -33,7 +34,7 @@ describe Projects::ForkService do
end
describe "successfully creates project in the user namespace" do
- let(:to_project) { fork_project(@from_project, @to_user) }
+ let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
@@ -60,13 +61,40 @@ describe Projects::ForkService do
expect(@from_project.forks_count).to eq(1)
end
+
+ it 'creates a fork network with the new project and the root project set' do
+ to_project
+ fork_network = @from_project.reload.fork_network
+
+ expect(fork_network).not_to be_nil
+ expect(fork_network.root_project).to eq(@from_project)
+ expect(fork_network.projects).to contain_exactly(@from_project, to_project)
+ end
+ end
+
+ context 'creating a fork of a fork' do
+ let(:from_forked_project) { fork_project(@from_project, @to_user) }
+ let(:other_namespace) do
+ group = create(:group)
+ group.add_owner(@to_user)
+ group
+ end
+ let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+
+ it 'sets the root of the network to the root project' do
+ expect(to_project.fork_network.root_project).to eq(@from_project)
+ end
+
+ it 'sets the forked_from_project on the membership' do
+ expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ end
end
end
context 'project already exists' do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
- @to_project = fork_project(@from_project, @to_user)
+ @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted
@@ -76,10 +104,11 @@ describe Projects::ForkService do
end
context 'repository already exists' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
before do
- gitlab_shell.add_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end
after do
@@ -87,7 +116,7 @@ describe Projects::ForkService do
end
it 'does not allow creation' do
- to_project = fork_project(@from_project, @to_user)
+ to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project).not_to be_persisted
expect(to_project.errors.messages).to have_key(:base)
@@ -181,9 +210,4 @@ describe Projects::ForkService do
end
end
end
-
- def fork_project(from_project, user, params = {})
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(from_project, user, params).execute
- end
end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb
new file mode 100644
index 00000000000..aa1988d29d6
--- /dev/null
+++ b/spec/services/projects/hashed_storage_migration_service_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Projects::HashedStorageMigrationService do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project) { create(:project, :empty_repo, :wiki_repo) }
+ let(:service) { described_class.new(project) }
+ let(:legacy_storage) { Storage::LegacyProject.new(project) }
+ let(:hashed_storage) { Storage::HashedProject.new(project) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
+ context 'when succeeds' do
+ it 'renames project and wiki repositories' do
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
+ end
+
+ it 'updates project to be hashed and not read-only' do
+ service.execute
+
+ expect(project.hashed_storage?).to be_truthy
+ expect(project.repository_read_only).to be_falsey
+ end
+
+ it 'move operation is called for both repositories' do
+ expect_move_repository(project.disk_path, hashed_storage.disk_path)
+ expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+
+ service.execute
+ end
+ end
+
+ context 'when one move fails' do
+ it 'rollsback repositories to original name' do
+ from_name = project.disk_path
+ to_name = hashed_storage.disk_path
+ allow(service).to receive(:move_repository).and_call_original
+ allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only
+
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
+ end
+
+ context 'when rollback fails' do
+ before do
+ from_name = legacy_storage.disk_path
+ to_name = hashed_storage.disk_path
+
+ hashed_storage.ensure_storage_path_exists
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ it 'does not try to move nil repository over hashed' do
+ expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+
+ service.execute
+ end
+ end
+ end
+
+ def expect_move_repository(from_name, to_name)
+ expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original
+ end
+ end
+end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 437c009e7fa..b7b5de07380 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -75,7 +75,7 @@ describe Projects::HousekeepingService do
end
end
- it 'uses all three kinds of housekeeping we offer' do
+ it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do
allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index a14ed526f68..2459f371a91 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -121,11 +121,14 @@ describe Projects::TransferService do
end
context 'namespace which contains orphan repository with same projects path name' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
before do
group.add_owner(user)
- gitlab_shell.add_repository(repository_storage_path, "#{group.full_path}/#{project.path}")
+ unless gitlab_shell.add_repository(repository_storage, "#{group.full_path}/#{project.path}")
+ raise 'failed to add repository'
+ end
@result = transfer_project(project, user, group)
end
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 4f1ab697460..50d3a4ec982 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -1,19 +1,22 @@
require 'spec_helper'
describe Projects::UnlinkForkService do
- subject { described_class.new(fork_project, user) }
+ include ProjectForksHelper
- let(:fork_link) { create(:forked_project_link) }
- let(:fork_project) { fork_link.forked_to_project }
+ subject { described_class.new(forked_project, user) }
+
+ let(:fork_link) { forked_project.forked_project_link }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { fork_project(project, user) }
let(:user) { create(:user) }
context 'with opened merge request on the source project' do
- let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) }
- let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) }
+ let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) }
+ let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
before do
allow(MergeRequests::CloseService).to receive(:new)
- .with(fork_project, user)
+ .with(forked_project, user)
.and_return(mr_close_service)
end
@@ -25,13 +28,24 @@ describe Projects::UnlinkForkService do
end
it 'remove fork relation' do
- expect(fork_project.forked_project_link).to receive(:destroy)
+ expect(forked_project.forked_project_link).to receive(:destroy)
+
+ subject.execute
+ end
+
+ it 'removes the link to the fork network' do
+ expect(forked_project.fork_network_member).to be_present
+ expect(forked_project.fork_network).to be_present
subject.execute
+ forked_project.reload
+
+ expect(forked_project.fork_network_member).to be_nil
+ expect(forked_project.reload.fork_network).to be_nil
end
it 'refreshes the forks count cache of the source project' do
- source = fork_project.forked_from_project
+ source = forked_project.forked_from_project
expect(source.forks_count).to eq(1)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 92cc9a37795..3da222e2ed8 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::UpdateService, '#execute' do
+ include ProjectForksHelper
+
let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -57,17 +59,26 @@ describe Projects::UpdateService, '#execute' do
end
end
end
+
+ context 'When project visibility is higher than parent group' do
+ let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+
+ before do
+ project.update(namespace: group, visibility_level: group.visibility_level)
+ end
+
+ it 'does not update project visibility level' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' })
+ expect(project.reload).to be_internal
+ end
+ end
end
describe 'when updating project that has forks' do
let(:project) { create(:project, :internal) }
- let(:forked_project) { create(:forked_project_with_submodules, :internal) }
-
- before do
- forked_project.build_forked_project_link(forked_to_project_id: forked_project.id,
- forked_from_project_id: project.id)
- forked_project.save
- end
+ let(:forked_project) { fork_project(project) }
it 'updates forks visibility level when parent set to more restrictive' do
opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
@@ -134,24 +145,43 @@ describe Projects::UpdateService, '#execute' do
end
context 'when renaming a project' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
- before do
- gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ context 'with legacy storage' do
+ before do
+ gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing")
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ it 'does not allow renaming when new path matches existing repository on disk' do
+ result = update_project(project, admin, path: 'existing')
+
+ expect(result).to include(status: :error)
+ expect(result[:message]).to match('There is already a repository with that name on disk')
+ expect(project).not_to be_valid
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ end
end
- it 'does not allow renaming when new path matches existing repository on disk' do
- result = update_project(project, admin, path: 'existing')
+ context 'with hashed storage' do
+ let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- expect(result).to include(status: :error)
- expect(result[:message]).to match('Project could not be updated!')
- expect(project).not_to be_valid
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ it 'does not check if new path matches existing repository on disk' do
+ expect(project).not_to receive(:repository_with_same_path_already_exists?)
+
+ result = update_project(project, admin, path: 'existing')
+
+ expect(result).to include(status: :success)
+ end
end
end
@@ -159,8 +189,10 @@ describe Projects::UpdateService, '#execute' do
it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' })
- expect(result).to eq({ status: :error,
- message: 'Project could not be updated!' })
+ expect(result).to eq({
+ status: :error,
+ message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
+ })
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index c2d6d7781b9..b1241cd8d0b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -232,7 +232,9 @@ describe SystemNoteService do
context 'when milestone added' do
it 'sets the note text' do
- expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
+ reference = milestone.to_reference(format: :iid)
+
+ expect(subject.note).to eq "changed milestone to #{reference}"
end
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index 57013b54560..e7e9080b6b0 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -28,7 +28,7 @@ describe Tags::CreateService do
it 'returns an error' do
expect(repository).to receive(:add_tag)
.with(user, 'v1.1.0', 'master', 'Foo')
- .and_raise(Rugged::TagError)
+ .and_raise(Gitlab::Git::Repository::TagExistsError)
response = service.execute('v1.1.0', 'master', 'Foo')
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index fef4da0c76e..17eabad73be 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -38,6 +38,18 @@ describe Users::ActivityService do
end
end
end
+
+ context 'when in GitLab read-only instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it 'does not update last_activity_at' do
+ service.execute
+
+ expect(last_hour_user_ids).to eq([])
+ end
+ end
end
def last_hour_user_ids
diff --git a/spec/services/users/last_push_event_service_spec.rb b/spec/services/users/last_push_event_service_spec.rb
new file mode 100644
index 00000000000..956358738fe
--- /dev/null
+++ b/spec/services/users/last_push_event_service_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Users::LastPushEventService do
+ let(:user) { build(:user, id: 1) }
+ let(:project) { build(:project, id: 2) }
+ let(:event) { build(:push_event, id: 3, author: user, project: project) }
+ let(:service) { described_class.new(user) }
+
+ describe '#cache_last_push_event' do
+ it "caches the event for the event's project and current user" do
+ expect(service).to receive(:set_key)
+ .ordered
+ .with('last-push-event/1/2', 3)
+
+ expect(service).to receive(:set_key)
+ .ordered
+ .with('last-push-event/1', 3)
+
+ service.cache_last_push_event(event)
+ end
+
+ it 'caches the event for the origin project when pushing to a fork' do
+ source = build(:project, id: 5)
+
+ allow(project).to receive(:forked?).and_return(true)
+ allow(project).to receive(:forked_from_project).and_return(source)
+
+ expect(service).to receive(:set_key)
+ .ordered
+ .with('last-push-event/1/2', 3)
+
+ expect(service).to receive(:set_key)
+ .ordered
+ .with('last-push-event/1', 3)
+
+ expect(service).to receive(:set_key)
+ .ordered
+ .with('last-push-event/1/5', 3)
+
+ service.cache_last_push_event(event)
+ end
+ end
+
+ describe '#last_event_for_user' do
+ it 'returns the last push event for the current user' do
+ expect(service).to receive(:find_cached_event)
+ .with('last-push-event/1')
+ .and_return(event)
+
+ expect(service.last_event_for_user).to eq(event)
+ end
+
+ it 'returns nil when no push event could be found' do
+ expect(service).to receive(:find_cached_event)
+ .with('last-push-event/1')
+ .and_return(nil)
+
+ expect(service.last_event_for_user).to be_nil
+ end
+ end
+
+ describe '#last_event_for_project' do
+ it 'returns the last push event for the given project' do
+ expect(service).to receive(:find_cached_event)
+ .with('last-push-event/1/2')
+ .and_return(event)
+
+ expect(service.last_event_for_project(project)).to eq(event)
+ end
+
+ it 'returns nil when no push event could be found' do
+ expect(service).to receive(:find_cached_event)
+ .with('last-push-event/1/2')
+ .and_return(nil)
+
+ expect(service.last_event_for_project(project)).to be_nil
+ end
+ end
+
+ describe '#find_cached_event', :use_clean_rails_memory_store_caching do
+ context 'with a non-existing cache key' do
+ it 'returns nil' do
+ expect(service.find_cached_event('bla')).to be_nil
+ end
+ end
+
+ context 'with an existing cache key' do
+ before do
+ service.cache_last_push_event(event)
+ end
+
+ it 'returns a PushEvent when no merge requests exist for the event' do
+ allow(service).to receive(:find_event_in_database)
+ .with(event.id)
+ .and_return(event)
+
+ expect(service.find_cached_event('last-push-event/1')).to eq(event)
+ end
+
+ it 'removes the cache key when no event could be found and returns nil' do
+ allow(PushEvent).to receive(:without_existing_merge_requests)
+ .and_return(PushEvent.none)
+
+ expect(Rails.cache).to receive(:delete)
+ .with('last-push-event/1')
+ .and_call_original
+
+ expect(service.find_cached_event('last-push-event/1')).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 6ee35a33b2d..f8d4a47b212 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -31,13 +31,13 @@ describe Users::UpdateService do
end
def update_user(user, opts)
- described_class.new(user, opts).execute
+ described_class.new(user, opts.merge(user: user)).execute
end
end
describe '#execute!' do
it 'updates the name' do
- service = described_class.new(user, name: 'New Name')
+ service = described_class.new(user, user: user, name: 'New Name')
expect(service).not_to receive(:notify_new_user)
result = service.execute!
@@ -55,7 +55,7 @@ describe Users::UpdateService do
it 'fires system hooks when a new user is saved' do
system_hook_service = spy(:system_hook_service)
user = build(:user)
- service = described_class.new(user, name: 'John Doe')
+ service = described_class.new(user, user: user, name: 'John Doe')
expect(service).to receive(:notify_new_user).and_call_original
expect(service).to receive(:system_hook_service).and_return(system_hook_service)
@@ -65,7 +65,7 @@ describe Users::UpdateService do
end
def update_user(user, opts)
- described_class.new(user, opts).execute!
+ described_class.new(user, opts.merge(user: user)).execute!
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ff1754fbe7e..48cacba6a8a 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,7 @@
require './spec/simplecov_env'
SimpleCovEnv.start!
-ENV["RAILS_ENV"] ||= 'test'
+ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
@@ -64,8 +64,16 @@ RSpec.configure do |config|
config.infer_spec_type_from_file_location!
- config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata|
- metadata[:api] = true
+ config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
+ location = metadata[:location]
+
+ metadata[:api] = true if location =~ %r{/spec/requests/api/}
+
+ # do not overwrite type if it's already set
+ next if metadata.key?(:type)
+
+ match = location.match(%r{/spec/([^/]+)/})
+ metadata[:type] = match[1].singularize.to_sym if match
end
config.raise_errors_for_deprecations!
@@ -73,7 +81,10 @@ RSpec.configure do |config|
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = 4
- config.reporter.register_listener(RspecFlaky::Listener.new, :example_passed, :dump_summary)
+ config.reporter.register_listener(
+ RspecFlaky::Listener.new,
+ :example_passed,
+ :dump_summary)
end
config.before(:suite) do
@@ -161,6 +172,24 @@ RSpec.configure do |config|
end
end
+# add simpler way to match asset paths containing digest strings
+RSpec::Matchers.define :match_asset_path do |expected|
+ match do |actual|
+ path = Regexp.escape(expected)
+ extname = Regexp.escape(File.extname(expected))
+ digest_regex = Regexp.new(path.sub(extname, "(?:-\\h+)?#{extname}") << '$')
+ digest_regex =~ actual
+ end
+
+ failure_message do |actual|
+ "expected that #{actual} would include an asset path for #{expected}"
+ end
+
+ failure_message_when_negated do |actual|
+ "expected that #{actual} would not include an asset path for #{expected}"
+ end
+end
+
FactoryGirl::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb
index 3bd589d64b9..57e28e040d7 100644
--- a/spec/support/api/scopes/read_user_shared_examples.rb
+++ b/spec/support/api/scopes/read_user_shared_examples.rb
@@ -23,6 +23,10 @@ shared_examples_for 'allows the "read_user" scope' do
context 'when the requesting token does not have any required scope' do
let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) }
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
it 'returns a "401" response' do
get api_call.call(path, user, personal_access_token: token)
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 81cb94ab8c4..9f05cabf7ae 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(page).not_to have_selector menu_selector
find(toggle_selector).click
- find('body').click
+ find('body').trigger 'click'
expect(page).not_to have_selector menu_selector
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 8282ba7e536..061e0d35590 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -29,7 +29,7 @@ shared_examples 'issuable record that supports quick actions in its description
wait_for_requests
end
- describe "new #{issuable_type}", js: true do
+ describe "new #{issuable_type}", :js do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
case issuable_type
@@ -53,7 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "note on #{issuable_type}", js: true do
+ describe "note on #{issuable_type}", :js do
before do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -290,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "preview of note on #{issuable_type}", js: true do
+ describe "preview of note on #{issuable_type}", :js do
it 'removes quick actions from note and explains them' do
create(:user, username: 'bob')
diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb
index 65b38626a51..3f7279a50e0 100644
--- a/spec/support/gpg_helpers.rb
+++ b/spec/support/gpg_helpers.rb
@@ -92,6 +92,46 @@ module GpgHelpers
KEY
end
+ def public_key_with_extra_signing_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1
+
+ mI0EWK7VJwEEANSFayuVYenl7sBKUjmIxwDRc3jd+K+FWUZgknLgiLcevaLh/mxV
+ 98dLxDKGDHHNKc/B7Y4qdlZYv1wfNQVuIbd8dqUQFOOkH7ukbgcGBTxH+2IM67y+
+ QBH618luS5Gz1d4bd0YoFf/xZGEh9G5xicz7TiXYzLKjnMjHu2EmbFePABEBAAG0
+ LU5hbm5pZSBCZXJuaGFyZCA8bmFubmllLmJlcm5oYXJkQGV4YW1wbGUuY29tPoi4
+ BBMBAgAiBQJYrtUnAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDM++Gf
+ AKyLHaeSA/99oUWpb02PlfkALcx5RncboMHkgczYEU9wOFIgvXIReswThCMOvPZa
+ piui+ItyJfV3ijJfO8IvbbFcvU7jjGA073Bb7tbzAEOQLA16mWgBLQlGaRWbHDW4
+ uwFxvkJKA0GzEsadEXeniESaZPc4rOXKPO3+/MSQWS2bmvvGsBTEuriNBFiu1ScB
+ BADIXkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUh
+ Yl6QXQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJit
+ LY4w/nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABiJ8EGAECAAkF
+ Aliu1ScCGwwACgkQzPvhnwCsix2WRQQAtOXpBS60myrBUXhlcqabDQgSTw+Spbgb
+ 61hEMckyzpk7SfMNLz0EbYMvj9SU6znBG8RGeUljPTVMxPGr9yIpoFMSPKAUi/0K
+ AgRmH3tVpxlMipwXjST1Jukk2eHckt/3jGw3E1ElMSFtULe6u5p4gu578hHukEwT
+ IKzj0ZyC7DK5AQ0EWcx23AEIANwpAq85bT10JCBuNhOMyF2jKVt5wHbI9wBtjWYG
+ fgJFBkRvm6IsbmR0Y5DSBvF/of0UX1iGMfx6mvCDJkb1okquhCUef6MONWRpzXYE
+ CIZDm1TXu6yv0D35tkLfPo+/sY9UHHp1zGRcPAU46e8ztRwoD+zEJwy7lobLHGOL
+ 9OdWtCGjsutLOTqKRK4jsifr8n3rePU09rejhDkRONNs7ufn9GRcWMN7RWiFDtpU
+ gNe84AJ38qaXPU8GHNTrDtDtRRPmn68ezMmE1qTNsxQxD4Isexe5Wsfc4+ElaP9s
+ zaHgij7npX1HS9RpmhnOa2h1ESroM9cqDh3IJVhf+eP6/uMAEQEAAYkBxAQYAQIA
+ DwUCWcx23AIbAgUJAeEzgAEpCRDM++GfAKyLHcBdIAQZAQIABgUCWcx23AAKCRDk
+ garE0uOuES7DCAC2Kgl6zO+NqIBIS6McgcEN0sGyvOvZ8Ps4hBiMwCyDAnsIRAUi
+ v4KZMtQMAyl9njJ3YjPWBsdieuTz45O06DDnrzJpZO5rUGJjAcEue4zvRRWIyu3H
+ qHC8MsvkslsNCygJHoWlknm+HucroskTNtxHQ+FdKZ6Tey+twl1u+PhV8PQVyFkl
+ 4G1chO90EP4dvYrye26CC+ik2JkvC7Vy5M+U0PJikme8pFMjcdNks25BnAKcdqKU
+ AU8RTkSjoYvb8qSmZyldJjYjQRkTPRX1ZdaOID1EdiWl+s5cn0Oypo3z7BChcEMx
+ IWB/gmXQJQXVr5qNQnJObyMO/RczXYi9qNnyGMED/2EJJERLR0nefjHQalwDKQVP
+ s5lX1OKAcf2CrV6ZarckqaQgtqjZbuV2C2mrOHUs5uojlXaopj5gA4yJSGDcYhj1
+ Rg9jdHWBtkHBj3eL32ZqrHDs3ap8ErZMmPE8A+mn9TTnQS+FY2QF7vBjJKM3qPT7
+ DMVGWrg4m1NF8N6yMPMP
+ =RB1y
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
def primary_keyid
fingerprint[-16..-1]
end
@@ -201,4 +241,277 @@ module GpgHelpers
['bette.cartwright@example.com', 'bette.cartwright@example.net']
end
end
+
+ # GPG Key with extra signing key
+ module User3
+ extend self
+
+ def signed_commit_signature
+ <<~SIGNATURE
+ -----BEGIN PGP SIGNATURE-----
+
+ iQEzBAABCAAdFiEEBSLdKbmPFnzYQhdS44/8r3Wr2SoFAlnNlT8ACgkQ44/8r3Wr
+ 2SqP1wf9FC4J2S8LIHs/fpxgkYzsyCp5lCbS7JuoD4pqmI2KWyBx+vi9/3mZPCsm
+ Fj9f0vFEtNOb39GNGZbaA8DdGw30/WAS6kI6yco0WSK53KHrLw9Kqd+3e/NAVSsl
+ 991Gq4n8X1U5izSH+gZOMtEEUBGqIlZKgRrEh7lhNcz0G7JTF2VCE4NNtZdq7GDA
+ N6jOQxDGUwi9wQBYORQzIBc3NihfhGloII1hXf0XzrgUY3zNYHTT7QipCxKAmH54
+ skwW+wi8RpBedar4saf7fs5xZbP/0yyVz98MJMdHBL68++Xt1AIHoqrb7eWszqnd
+ PCo/fnz1iHKCig602KLj0/zhADcNkg==
+ =LsTi
+ -----END PGP SIGNATURE-----
+ SIGNATURE
+ end
+
+ def signed_commit_base_data
+ <<~SIGNEDDATA
+ tree 86ec18bfe87ad42a782fdabd8310f9b7ac750f51
+ parent 2d1096e3a0ecf1d2baf6dee036cc80775d4940ba
+ author John Doe <john.doe@example.com> 1506645311 -0500
+ committer John Doe <john.doe@example.com> 1506645311 -0500
+
+ Commit signed with subkey by John Doe
+ SIGNEDDATA
+ end
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnNgbIBCAC9/WblcR4s/pFTwh9cm2iS59YRhtGfbrnfNE6QMIFIRFaK0u6J
+ LDy+scipXLoGg7X0XNFLW6Y457UUpkaPDVLPuVMONhMXdVqndGVvk9jq3D4oDtRa
+ ukucuXr9F84lHnjL3EosmAUiK3xBmHOGHm8+bvKbgPOdvre48YxoJ1POTen+chfo
+ YtLUfnC9EEGY/bn00aaXm3NV+zZK2zio5bFX9YLWSNh/JaXxuJsLoHR/lVrU7CLt
+ FCaGcPQ9SU46LHPshEYWO7ZsjEYJsYYOIOEzfcfR47T2EDTa6mVc++gC5TCoY3Ql
+ jccgm+EM0LvyEHwupEpxzCg2VsT0yoedhUhtABEBAAG0H0pvaG4gRG9lIDxqb2hu
+ LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQTqP4uIlyqP1HSHLy8RWzrxqtPt
+ ugUCWc2BsgIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRARWzrx
+ qtPturMDCACc1Pi1sLJFcCnJEc9sCInCO4LH8fntNjRLN3MTPU5YCYmFM3fAl5ly
+ vXPZ4jNWZxKbQVeFnkDOg5Ti8bzmFEMc8KbZuguktVFizxnLdFCTTRO89i3HDVSN
+ bIosrs5HJwRKOzul6i2whn3dsr8/P8WJczYjZGiw29hGwH3md4Thn/aAGbzjoCEF
+ IfIb1kccyHMJkaj79S8B2agsbEJLuTSfsXC3kGZIJyKG1DNmDUHW/ZE6ro/Kkhik
+ 3w6Jw14cMsKUIOBkOgsD/gXgX9xxWjYHmKrbCXucTKUevNlaCy5tzwrC0Am3prl9
+ OJj3macOA8hNaTVDflEHNCwHOwqnVQYyuQENBFnNgbIBCAC59MmKc0cqPRPTpCZ5
+ arKRoj23SNKWMDWaxSELdU91Wd/NJk4wF25urk9BtBuwjzaBMqb/xPD88yJC3ucs
+ 2LwCyAb5C/dHcPOpys8Pd+KrdHDR3zAMjcASsizlW/qFI9MtjhcU9Yd6iTUejAZG
+ NEC76WALJ3SLYzCaDkHFjWpH3Xq6ck3/9jpL3csn/5GLCsZQUDYGrZSXvHAIigwW
+ Xo6tMs5LCCO9CZg2qGDpvqlzcmy6CRkf0h/UFYJzGqfbJtxeCIxa93WIPE8eGwao
+ aneDlNtIoYiP6krC3OLsaPWT58QltNKaQuZSpjwtQBHa4JIt55vx+FcvRb7Kflgf
+ fT8bABEBAAGJATwEGAEIACYWIQTqP4uIlyqP1HSHLy8RWzrxqtPtugUCWc2BsgIb
+ DAUJA8JnAAAKCRARWzrxqtPtuqJjCACj+Z4qtgMpJXx3u58wCzkVLl5IylD/tEPA
+ cNIrj8QS8ec+woTJaMGVCh96VC2FPl8KR4Hjhy0yaupyPbTI6VWib63S/NcDfG7r
+ tviRFG2Gf8yduERebyC0cpgnmjVgFfJs7N3K3ncz6myOr9idNI05OC9poL73sDUv
+ jRXhm7uy938bT/R4MQdpYuxucgU3MiwvfG5ht+oJ4Yp+/IrR2PTqRGqMCsodnroa
+ TBKq2kW565TtCvrFkNub/ytorDbIVN9VrIMcuTiOv8sLoQRDJO9HvWUhYAqMY6Uh
+ dy12jR9FrquZnGsDKKs9V0Y6J4Wi8vnmdgWVZUc40pfXq3APAB6suQENBFnNgeAB
+ CADFqQRxLHxLIQ7B72diTMI2tPk9d5c67k+Gzkrg1QYoxBLdRCmhM4ydYqZzvIz4
+ 1npkK20w4somOCwvyAOjO46IGb3f/Wk8mY8o5HMpI1miAfze0YTZKzRo2DmrvwbV
+ /h8jvZNCISwtrOgaaszWSVSuEQQCA1jf4qixfCb3ReETvZc3MTZXhw8IUbszXh5d
+ a6CYqq/fr5Zw4Dc19ZSoHSTh0Wn03mEm/kaYtia/wt1I+xVvTSaC2Pf/kUtr7UEf
+ 3NMc0YF0s4KgeW8KwjHa7Sz9wzydLPRH5kJ26SDUGUhrFf1jNLIegtDsoISV/86G
+ ztXcVq5GY6lXMwmsggRe++7xABEBAAGJAmwEGAEIACAWIQTqP4uIlyqP1HSHLy8R
+ WzrxqtPtugUCWc2B4AIbAgFACRARWzrxqtPtusB0IAQZAQgAHRYhBAUi3Sm5jxZ8
+ 2EIXUuOP/K91q9kqBQJZzYHgAAoJEOOP/K91q9kqlHcH+wbvD14ITYDMfgIfy67O
+ 4Qcmgf1qzGXhpsABz/i/EPgRD990eNBI0YGuvoKRJfetEGn7LerrrCB8Z+ICFPHF
+ rzXoe10zm+QTREck0OB8nPFRycJ+Fbl6JX+cnkEx27Mmg1aVb7+H5LMDaWO1KjLs
+ g2wIDo/jrDfW7NoZzy4XTd7jFCOt4fftL/dhiujQmhLzugZXCxRISOVdmgilDJQo
+ Tz1sEm34ida98JFjdzSgkUvJ/pFTZ21ThCNxlUf01Hr2Pdcg1e2/97sZocMFTY2J
+ KwmiW2LG3B05/VrRtdvsCVj8G49coxgPPKty+m71ovAXdS/CvVqE7TefCplsYJ1d
+ V3abwwf/Xl2SxzbAKbrYMgZfdGzpPg2u6982WvfGIVfjFJh9veHZAbfkPcjdAD2X
+ e67Y4BeKG2OQQqeOY2y+81A7PaehgHzbFHJG/4RjqB66efrZAg4DgIdbr4oyMoUJ
+ VVsl0wfYSIvnd4kvWXYICVwk53HLA3wIowZAsJ1LT2byAKbUzayLzTekrTzGcwQk
+ g2XT798ev2QuR5Ki5x8MULBFX4Lhd03+uGOxjhNPQD6DAAHCQLaXQhaGuyMgt5hD
+ t0nF3yuw3Eg4Ygcbtm24rZXOHJc9bDKeRiWZz1dIyYYVJmHSQwOVXkAwJlF1sIgy
+ /jQYeOgFDKq20x86WulkvsUtBoaZJg==
+ =Q5Z7
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~SECRET
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQPGBFnNgbIBCAC9/WblcR4s/pFTwh9cm2iS59YRhtGfbrnfNE6QMIFIRFaK0u6J
+ LDy+scipXLoGg7X0XNFLW6Y457UUpkaPDVLPuVMONhMXdVqndGVvk9jq3D4oDtRa
+ ukucuXr9F84lHnjL3EosmAUiK3xBmHOGHm8+bvKbgPOdvre48YxoJ1POTen+chfo
+ YtLUfnC9EEGY/bn00aaXm3NV+zZK2zio5bFX9YLWSNh/JaXxuJsLoHR/lVrU7CLt
+ FCaGcPQ9SU46LHPshEYWO7ZsjEYJsYYOIOEzfcfR47T2EDTa6mVc++gC5TCoY3Ql
+ jccgm+EM0LvyEHwupEpxzCg2VsT0yoedhUhtABEBAAH+BwMCOqjIWtWBMo3mjIP1
+ OnIbZ+YJxSUZ/B8wU2loMv4XiKmeXLbjD6h3jojxYlnreSHA9QvoY8uNaWElL/n2
+ jv6bxluivk8tA9FWJVv4HaSlMDd2J2YmUW17r8z9Kvm7b7pFVSrEoYV93Wdj5FJ7
+ ciKrFhYNSD7tH1sHwkrFAbiv6aHyk9h48YmR3kx2wBvz+pWk7M2srCJx2b6DXkj/
+ fsj1c/vnzUUGooOJgOvYAWrpg/rJUNxSsFypAHf8Xtk+xt8S1aZ9jaCmYk6I1B2L
+ m00HP43cXUpKcmETW1zXvfMLKjjoUEAJhSJhbCwiEzGL4ojQTarl8qbb+MisakEJ
+ DkPYtrhiiuVzUIFfqE86yO0UKidtzBmJAW3c6zeiUATvACzU09aGyUY1cJi93oXD
+ w4PCyVZ+nMvGD1wx+gyYdDINwpX4y6od9RDr06DGCzwu+S2vxsu1T8LdSv52fhBr
+ U0FY3Z3VN1ytay4SHi/8Y9VBYQFBh7R7Ch0gEMxLVKXVNqOXHUdGrKWV/WmyLKuZ
+ W9DEnWU4Mpz/di5jU8EDW7EB9DZZhVk3mQw3nuAZrBGD4azmmD5mgSgLeBGmKZ1e
+ q/9IWO44mRBBUtNv+rAkmmYF3MCNHuc7EMj+c/IgBUC7d5qBzGWA3UJ0vKX4xcIQ
+ X/PnU+nGxNvBrdqQaMLczeg28SerojxuX79prOsoySctLAbajd9HshW5SfOZ0rvb
+ BNHPqolQDijYEHGxANh4BbamRMGi60Rop7vJsZOLAemz17x/mvCtAHISOJT77/IM
+ oWC+IksJ5XsA/klJGe/tkx11aRQDDmKvIJXmMuRdvnIR23UBbzRQlWWq0l6CdoF6
+ 6SQ9BJBFq0WY32No9WZAPnDO3buUzWc1Y3uwn/+h7TVYVyTlEqzpYJ9FoJwBHbor
+ 0663eoyz6+AUtB9Kb2huIERvZSA8am9obi5kb2VAZXhhbXBsZS5jb20+iQFUBBMB
+ CAA+FiEE6j+LiJcqj9R0hy8vEVs68arT7boFAlnNgbICGwMFCQPCZwAFCwkIBwIG
+ FQgJCgsCBBYCAwECHgECF4AACgkQEVs68arT7bqzAwgAnNT4tbCyRXApyRHPbAiJ
+ wjuCx/H57TY0SzdzEz1OWAmJhTN3wJeZcr1z2eIzVmcSm0FXhZ5AzoOU4vG85hRD
+ HPCm2boLpLVRYs8Zy3RQk00TvPYtxw1UjWyKLK7ORycESjs7peotsIZ93bK/Pz/F
+ iXM2I2RosNvYRsB95neE4Z/2gBm846AhBSHyG9ZHHMhzCZGo+/UvAdmoLGxCS7k0
+ n7Fwt5BmSCcihtQzZg1B1v2ROq6PypIYpN8OicNeHDLClCDgZDoLA/4F4F/ccVo2
+ B5iq2wl7nEylHrzZWgsubc8KwtAJt6a5fTiY95mnDgPITWk1Q35RBzQsBzsKp1UG
+ Mp0DxgRZzYGyAQgAufTJinNHKj0T06QmeWqykaI9t0jSljA1msUhC3VPdVnfzSZO
+ MBdubq5PQbQbsI82gTKm/8Tw/PMiQt7nLNi8AsgG+Qv3R3DzqcrPD3fiq3Rw0d8w
+ DI3AErIs5Vv6hSPTLY4XFPWHeok1HowGRjRAu+lgCyd0i2Mwmg5BxY1qR916unJN
+ //Y6S93LJ/+RiwrGUFA2Bq2Ul7xwCIoMFl6OrTLOSwgjvQmYNqhg6b6pc3JsugkZ
+ H9If1BWCcxqn2ybcXgiMWvd1iDxPHhsGqGp3g5TbSKGIj+pKwtzi7Gj1k+fEJbTS
+ mkLmUqY8LUAR2uCSLeeb8fhXL0W+yn5YH30/GwARAQAB/gcDAuYn/gmAA3OC5p5Q
+ Pat5kE7MtmSvSPmdjVA2o+6RtqZf81JqtAgtDVDwj7SPFsH6ue5P+iAn9938YYek
+ WQU2+0GXeUbSJt+u4VAchgwA5mYsEnEr1/E5KEfWPWO3jJol1rJG99adrjkMxvug
+ QJmwieqhu0368w1FU0tKstxYbr3Tz3nPCPDJoigMEUkXiFklDCUgeNk0g+zd5ytE
+ lXuuLYcGZX7njxL5jD+cMIKqua5zv8WbvNf/BhM1nCarxp4qzKWim8J8jY+iR+/E
+ qOar4aliGRez0j+qh/r8ywgPwfOO89zrKrMfaclL7dN9yuecmBHKWZvfrP5JKMHj
+ yTU3nRMhUGbfVUaaZI2Ccz2rNOU4oF9wuzpzQi8qOysZixRmH61Nw3ULIKoQgiWp
+ 0p5A3L94OaEfZEq3plVaIXI2YWYFSEAlIAc2dq4GxynousLdhNACi9bHhXrfFUhK
+ ckw1QlbhguO/j63/x8ygsmLZVwHG0fJZtMhT3+EGam9cuMBibIYyu3ufJRy7kMKt
+ kmyuk02X+hYJ7w8Pu6b8zYHBXbsEKamipMgd4oKtc8WnXILZo4lwDSArgs7ZVCBa
+ vGBbpTOsr54WjsyuCdX/wv0F2l31J87UxVtTKXx/+nfMfCE02zd+NsTgqvgqmkaA
+ Sy3qvv326kJNx7p+5hRwDzlAZ7vGJjj5TwCbGYDvctIf6MFrGDRNYwrGwNkPc3TG
+ rturfeL/ioua0Smj8LIbOv9Ir93gUIseNpxv8tXV/lffdIplcw802b3aXIKyv4fq
+ b9y3Oq/pDHFukKuBe9WTXJvjT0+ME+a0C8KIb/sts95pmjZsgN1kPmvuT0ReQwUR
+ eGrqz387bnVUzo4RgM3IERs/0EYzPzE8A2vc1e4/87b5J+1Xnov8Phd29vW8Td5l
+ ApiFrFO2r+/Np4kBPAQYAQgAJhYhBOo/i4iXKo/UdIcvLxFbOvGq0+26BQJZzYGy
+ AhsMBQkDwmcAAAoJEBFbOvGq0+26omMIAKP5niq2AyklfHe7nzALORUuXkjKUP+0
+ Q8Bw0iuPxBLx5z7ChMlowZUKH3pULYU+XwpHgeOHLTJq6nI9tMjpVaJvrdL81wN8
+ buu2+JEUbYZ/zJ24RF5vILRymCeaNWAV8mzs3credzPqbI6v2J00jTk4L2mgvvew
+ NS+NFeGbu7L3fxtP9HgxB2li7G5yBTcyLC98bmG36gnhin78itHY9OpEaowKyh2e
+ uhpMEqraRbnrlO0K+sWQ25v/K2isNshU31Wsgxy5OI6/ywuhBEMk70e9ZSFgCoxj
+ pSF3LXaNH0Wuq5mcawMoqz1XRjonhaLy+eZ2BZVlRzjSl9ercA8AHqydA8YEWc2B
+ 4AEIAMWpBHEsfEshDsHvZ2JMwja0+T13lzruT4bOSuDVBijEEt1EKaEzjJ1ipnO8
+ jPjWemQrbTDiyiY4LC/IA6M7jogZvd/9aTyZjyjkcykjWaIB/N7RhNkrNGjYOau/
+ BtX+HyO9k0IhLC2s6BpqzNZJVK4RBAIDWN/iqLF8JvdF4RO9lzcxNleHDwhRuzNe
+ Hl1roJiqr9+vlnDgNzX1lKgdJOHRafTeYSb+Rpi2Jr/C3Uj7FW9NJoLY9/+RS2vt
+ QR/c0xzRgXSzgqB5bwrCMdrtLP3DPJ0s9EfmQnbpINQZSGsV/WM0sh6C0OyghJX/
+ zobO1dxWrkZjqVczCayCBF777vEAEQEAAf4HAwKESvCIDq5QNeadnSvpkZemItPO
+ lDf+7Wiue2gt776D5xkVyT7WkgTQv+IGWGtqz7pCCO2rMp/F9u1BghdjY46jtrK6
+ MMFKta4YENUhRliH6M2YmRjq5p7xZgH6UOnDlqsafbfyUx30t59tbQj+07aMnH5J
+ LMm37nVkDvo3wpPQPuo7L6qizYsrHrQKeJZ8636u41UjC99lVH7vXzqXw68FJImi
+ XdMZbEVBIprYfCDem+fD6gJBA4JBqWJMxuFMfhWp+1WtYoeNojDm4KxBzc2fvYV/
+ HOIUfLFBvACD/UwU5ovllHN39/O8SMgyLm9ymx2/qXcdIkUz4l7fhOCY1OW12DMu
+ 5OFrrTteGK/Sj4Z8pYRdMdaKyjIlxuVzEQGWsU5+J2ALao5atEHguqwlD3cKh3G8
+ 1sA/l5eTFDt84erYv1MVStV0BhZaCE4mNL4WpnQGDdW05yoGq9jIyLcurb/k/atU
+ TUkAF1csgNlJlR3IP+7U9xfHkjMO5+SV82xoNf9nBjz06TRdnvOSKsMNKp0RxC/L
+ Hbiee9o7Rxqdiyv0ly6bCCymwfvlsEIqo3YKssBfe3XI5yQI2hF9QZaH1ywzmgaH
+ o+rbME/XxddRJueS79eipT7K05Z3ulSHTXzpDw+jZcdUV0Ac72Q9FTDPMl3xc6NW
+ DrYwWw/3+kyZ4SkP56l7KlGczTyNPvU9iou4Cj/cAZk/pHx68Chq8ZZNznFm/bIF
+ gWt3fqE/n+y78B6MI8qTjGJOR0jycxrLH82Z2F+FpMShI2C5NnOa/Ilkv3e2Q5U6
+ MOAwaCIz6RHhcI99O/yta2vLelWZqn2g86rLzTG0HlIABTCPYotwetHh0hsrkSv9
+ Kh6rOzGB4i8lRqcBVY+alMSiRBlzkwpL4YUcO6f3vEDncQ9evE1AQCpD4jUJjB1H
+ JSSJAmwEGAEIACAWIQTqP4uIlyqP1HSHLy8RWzrxqtPtugUCWc2B4AIbAgFACRAR
+ WzrxqtPtusB0IAQZAQgAHRYhBAUi3Sm5jxZ82EIXUuOP/K91q9kqBQJZzYHgAAoJ
+ EOOP/K91q9kqlHcH+wbvD14ITYDMfgIfy67O4Qcmgf1qzGXhpsABz/i/EPgRD990
+ eNBI0YGuvoKRJfetEGn7LerrrCB8Z+ICFPHFrzXoe10zm+QTREck0OB8nPFRycJ+
+ Fbl6JX+cnkEx27Mmg1aVb7+H5LMDaWO1KjLsg2wIDo/jrDfW7NoZzy4XTd7jFCOt
+ 4fftL/dhiujQmhLzugZXCxRISOVdmgilDJQoTz1sEm34ida98JFjdzSgkUvJ/pFT
+ Z21ThCNxlUf01Hr2Pdcg1e2/97sZocMFTY2JKwmiW2LG3B05/VrRtdvsCVj8G49c
+ oxgPPKty+m71ovAXdS/CvVqE7TefCplsYJ1dV3abwwf/Xl2SxzbAKbrYMgZfdGzp
+ Pg2u6982WvfGIVfjFJh9veHZAbfkPcjdAD2Xe67Y4BeKG2OQQqeOY2y+81A7Paeh
+ gHzbFHJG/4RjqB66efrZAg4DgIdbr4oyMoUJVVsl0wfYSIvnd4kvWXYICVwk53HL
+ A3wIowZAsJ1LT2byAKbUzayLzTekrTzGcwQkg2XT798ev2QuR5Ki5x8MULBFX4Lh
+ d03+uGOxjhNPQD6DAAHCQLaXQhaGuyMgt5hDt0nF3yuw3Eg4Ygcbtm24rZXOHJc9
+ bDKeRiWZz1dIyYYVJmHSQwOVXkAwJlF1sIgy/jQYeOgFDKq20x86WulkvsUtBoaZ
+ Jg==
+ =TKlF
+ -----END PGP PRIVATE KEY BLOCK-----
+ SECRET
+ end
+
+ def fingerprint
+ 'EA3F8B88972A8FD474872F2F115B3AF1AAD3EDBA'
+ end
+
+ def subkey_fingerprints
+ %w(159AD5DDF199591D67D2B87AA3CEC5C0A7C270EC 0522DD29B98F167CD8421752E38FFCAF75ABD92A)
+ end
+
+ def names
+ ['John Doe']
+ end
+
+ def emails
+ ['john.doe@example.com']
+ end
+ end
+
+ # GPG Key containing just the main key
+ module User4
+ extend self
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAG0G0pvaG4gRG9lIDxqb2hu
+ QGV4YW1wbGUuY29tPokBTgQTAQgAOBYhBAh0izYM0lwuzJnVlAcBbPnhOj+bBQJZ
+ 1nHrAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEAcBbPnhOj+bkywH/i4w
+ OwpDxoTjUQlPlqGAGuzvWaPzSJndawgmMTr68oRsD+wlQmQQTR5eqxCpUIyV4aYb
+ D697RYzoqbT4mlU49ymzfKSAxFe88r1XQWdm81DcofHVPmw2GBrIqaX3Du4Z7xkI
+ Q9/S43orwknh5FoVwU8Nau7qBuv9vbw2apSkuA1oBj3spQ8hqwLavACyQ+fQloAT
+ hSDNqPiCZj6L0dwM1HYiqVoN3Q7qjgzzeBzlXzljJoWblhxllvMK20bVoa7H+uR2
+ lczFHfsX8VTIMjyTGP7R3oHN91DEahlQybVVNLmNSDKZM2P/0d28BRUmWxQJ4Ws3
+ J4hOWDKnLMed3VOIWzM=
+ =xVuW
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~KEY.strip
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQPGBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAH+BwMC4UwgHgH5Cp7meY39
+ G5Q3GV2xtwADoaAvlOvPOLPK2fQqxQfb4WN4eZECp2wQuMRBMj52c4i9yphab1mQ
+ vOzoPIRGvkcJoxG++OxQ0kRk0C0gX6wM6SGVdb1nQnfZnoJCCU3IwCaSGktkLDs1
+ jwdI+VmXJbSugUbd25bakHQcE2BaNHuRBlQWQfFbhGBy0+uMfNDBZ6FRipBu47hO
+ f/wm/xXuV8N8BSgvNR/qtAqSQI34CdsnWAhMYm9rqmTNyt0nq4dveX+E0YzVn4lH
+ lOEa7cpYeuBwIL8L3EvSPNCICiJlF3gVqiYzyqRElnCkv1OGc0x3W5onY/agHgGZ
+ KYyi/ubOdqqDgBR+eMt0JKSGH2EPxUAGFPY5F37u4erdxH86GzIinAExLSmADiVR
+ KtxluZP6S2KLbETN5uVbrfa+HVcMbbUZaBHHtL+YbY8PqaFUIvIUR1HM2SK7IrFw
+ KuQ8ibRgooyP7VgMNiPzlFpY4NXUv+FXIrNJ6ELuIaENi0izJ7aIbVBM8SijDz6u
+ 5EEmodnDvmU2hmQNZJ17TxggE7oeT0rKdDGHM5zBvqZ3deqE9sgKx/aTKcj61ID3
+ M80ZkHPDFazUCohLpYgFN20bYYSmxU4LeNFy8YEiuic8QQKaAFxSf9Lf87UFQwyF
+ dduI1RWEbjMsbEJXwlmGM02ssQHsgoVKwZxijq5A5R1Ul6LowazQ8obPiwRS4NZ4
+ Z+QKDon79MMXiFEeh1jeG/MKKWPxFg3pdtCWhC7WdH4hfkBsCVKf+T58yB2Gzziy
+ fOHvAl7v3PtdZgf1xikF8spGYGCWo4B2lxC79xIflKAb2U6myb5I4dpUYxzxoMxT
+ zxHwxEie3NxzZGUyXSt3LqYe2r4CxWnOCXWjIxxRlLue1BE5Za1ycnDRjgUO24+Z
+ uDQne6KLkhAotBtKb2huIERvZSA8am9obkBleGFtcGxlLmNvbT6JAU4EEwEIADgW
+ IQQIdIs2DNJcLsyZ1ZQHAWz54To/mwUCWdZx6wIbAwULCQgHAgYVCAkKCwIEFgID
+ AQIeAQIXgAAKCRAHAWz54To/m5MsB/4uMDsKQ8aE41EJT5ahgBrs71mj80iZ3WsI
+ JjE6+vKEbA/sJUJkEE0eXqsQqVCMleGmGw+ve0WM6Km0+JpVOPcps3ykgMRXvPK9
+ V0FnZvNQ3KHx1T5sNhgayKml9w7uGe8ZCEPf0uN6K8JJ4eRaFcFPDWru6gbr/b28
+ NmqUpLgNaAY97KUPIasC2rwAskPn0JaAE4Ugzaj4gmY+i9HcDNR2IqlaDd0O6o4M
+ 83gc5V85YyaFm5YcZZbzCttG1aGux/rkdpXMxR37F/FUyDI8kxj+0d6BzfdQxGoZ
+ UMm1VTS5jUgymTNj/9HdvAUVJlsUCeFrNyeITlgypyzHnd1TiFsz
+ =/37z
+ -----END PGP PRIVATE KEY BLOCK-----
+ KEY
+ end
+
+ def primary_keyid
+ fingerprint[-16..-1]
+ end
+
+ def fingerprint
+ '08748B360CD25C2ECC99D59407016CF9E13A3F9B'
+ end
+ end
end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
new file mode 100644
index 00000000000..fd22e384b1b
--- /dev/null
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -0,0 +1,28 @@
+module MergeRequestDiffHelpers
+ def click_diff_line(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ line[:num].find('.add-diff-note').trigger('click')
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+end
diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/ldap_shared_examples.rb
new file mode 100644
index 00000000000..52c34e78965
--- /dev/null
+++ b/spec/support/ldap_shared_examples.rb
@@ -0,0 +1,69 @@
+shared_examples_for 'normalizes a DN' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_description, :given, :expected) do
+ 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith'
+ 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith, ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith,ou=people (aka. \\"humans\\"),dc=example,dc=com'
+ 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'for a null DN (empty string), returns empty string and does not error' | '' | ''
+ 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com'
+ 'does not strip an escaped leading space in the last attribute value' | 'uid=\\ John Smith' | 'uid=\\ john smith'
+ 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com'
+ 'strips extraneous spaces after an escaped trailing space' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com'
+ 'strips extraneous spaces after an escaped trailing space at the end of the DN' | 'uid=John Smith,ou=People,dc=example,dc=com\\ ' | 'uid=john smith,ou=people,dc=example,dc=com\\ '
+ 'properly preserves escaped trailing space after unescaped trailing spaces' | 'uid=John Smith \\ ,ou=People,dc=example,dc=com' | 'uid=john smith \\ ,ou=people,dc=example,dc=com'
+ 'preserves multiple inner spaces in an attribute value' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'preserves inner spaces after an escaped space' | 'uid=John\\ Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com"
+ 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com"
+ 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com"
+ 'strips an unescaped trailing newline (actually an invalid DN?)' | "uid=John Smith\n,ou=People,dc=example,dc=com" | "uid=john smith,ou=people,dc=example,dc=com"
+ 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar'
+ 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar'
+ 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca'
+ 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca'
+ 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca'
+ 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca'
+ 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca'
+ 'allows attribute type name OIDs' | '0.9.2342.19200300.100.1.25=Example,0.9.2342.19200300.100.1.25=Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com'
+ 'strips extraneous whitespace from attribute type name OIDs' | '0.9.2342.19200300.100.1.25 = Example, 0.9.2342.19200300.100.1.25 = Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com'
+ end
+
+ with_them do
+ it 'normalizes the DN' do
+ assert_generic_test(test_description, subject, expected)
+ end
+ end
+end
+
+shared_examples_for 'normalizes a DN attribute value' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_description, :given, :expected) do
+ 'strips extraneous whitespace' | ' John Smith ' | 'john smith'
+ 'unescapes non-reserved, non-special Unicode characters' | 'Sebasti\\c3\\a1n\\ C.\\20Smith' | 'sebastián c. smith'
+ 'downcases the whole string' | 'JoHn C. Smith' | 'john c. smith'
+ 'does not strip an escaped leading space in an attribute value' | '\\ John Smith' | '\\ john smith'
+ 'does not strip an escaped trailing space in an attribute value' | 'John Smith\\ ' | 'john smith\\ '
+ 'hex-escapes an escaped leading newline in an attribute value' | "\\\nJohn Smith" | "\\0ajohn smith"
+ 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "John Smith\\\n" | "john smith\\0a"
+ 'hex-escapes an unescaped leading newline (actually an invalid DN value?)' | "\nJohn Smith" | "\\0ajohn smith"
+ 'strips an unescaped trailing newline (actually an invalid DN value?)' | "John Smith\n" | "john smith"
+ 'does not strip if no extraneous whitespace' | 'John Smith' | 'john smith'
+ 'does not modify an escaped equal sign in an attribute value' | ' foo \\= bar' | 'foo \\= bar'
+ 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | ' foo \\3D bar' | 'foo \\= bar'
+ 'does not modify an escaped comma in an attribute value' | 'San Francisco\\, CA' | 'san francisco\\, ca'
+ 'converts an escaped hex comma to an escaped comma in an attribute value' | 'San Francisco\\2C CA' | 'san francisco\\, ca'
+ 'does not modify an escaped hex carriage return character in an attribute value' | 'San Francisco\\,\\0DCA' | 'san francisco\\,\\0dca'
+ 'does not modify an escaped hex line feed character in an attribute value' | 'San Francisco\\,\\0ACA' | 'san francisco\\,\\0aca'
+ 'does not modify an escaped hex CRLF in an attribute value' | 'San Francisco\\,\\0D\\0ACA' | 'san francisco\\,\\0d\\0aca'
+ end
+
+ with_them do
+ it 'normalizes the DN attribute value' do
+ assert_generic_test(test_description, subject, expected)
+ end
+ end
+end
diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb
new file mode 100644
index 00000000000..63f59b9654c
--- /dev/null
+++ b/spec/support/matchers/navigation_matcher.rb
@@ -0,0 +1,12 @@
+RSpec::Matchers.define :have_active_navigation do |expected|
+ match do |page|
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected)
+ end
+end
+
+RSpec::Matchers.define :have_active_sub_navigation do |expected|
+ match do |page|
+ expect(page.find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(expected)
+ end
+end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
index 4ca019c1b05..6522d74ba89 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/migrations_helpers.rb
@@ -16,7 +16,9 @@ module MigrationsHelpers
end
def reset_column_in_migration_models
- ActiveRecord::Base.clear_cache!
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ conn.schema_cache.clear!
+ end
described_class.constants.sort.each do |name|
const = described_class.const_get(name)
diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb
new file mode 100644
index 00000000000..0d1c6792d13
--- /dev/null
+++ b/spec/support/project_forks_helper.rb
@@ -0,0 +1,58 @@
+module ProjectForksHelper
+ def fork_project(project, user = nil, params = {})
+ # Load the `fork_network` for the project to fork as there might be one that
+ # wasn't loaded yet.
+ project.reload unless project.fork_network
+
+ unless user
+ user = create(:user)
+ project.add_developer(user)
+ end
+
+ unless params[:namespace] || params[:namespace_id]
+ params[:namespace] = create(:group)
+ params[:namespace].add_owner(user)
+ end
+
+ service = Projects::ForkService.new(project, user, params)
+
+ create_repository = params.delete(:repository)
+ # Avoid creating a repository
+ unless create_repository
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ shell = double('gitlab_shell', fork_repository: true)
+ allow(service).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ forked_project = service.execute
+
+ # Reload the both projects so they know about their newly created fork_network
+ if forked_project.persisted?
+ project.reload
+ forked_project.reload
+ end
+
+ if create_repository
+ # The call to project.repository.after_import in RepositoryForkWorker does
+ # not reset the @exists variable of this forked_project.repository
+ # so we have to explicitely call this method to clear the @exists variable.
+ # of the instance we're returning here.
+ forked_project.repository.after_import
+
+ # We can't leave the hooks in place after a fork, as those would fail in tests
+ # The "internal" API is not available
+ FileUtils.rm_rf("#{forked_project.repository.path}/hooks")
+ end
+
+ forked_project
+ end
+
+ def fork_project_with_submodules(project, user = nil, params = {})
+ forked_project = fork_project(project, user, params)
+ TestEnv.copy_repo(forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
+
+ forked_project
+ end
+end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index 55b531b4cf7..ba0b805caad 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -34,15 +34,47 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
match do |block|
- query_count(&block) > expected
+ query_count(&block) > expected_count + threshold
end
failure_message_when_negated do |actual|
- "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}"
+ threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
+ counts = "#{expected_count}#{threshold_message}"
+ "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
+ end
+
+ def with_threshold(threshold)
+ @threshold = threshold
+ self
+ end
+
+ def threshold
+ @threshold.to_i
+ end
+
+ def expected_count
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ expected.count
+ else
+ expected
+ end
+ end
+
+ def actual_count
+ @recorder.count
end
def query_count(&block)
@recorder = ActiveRecord::QueryRecorder.new(&block)
@recorder.count
end
+
+ def log_message
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ extra_queries = (expected.log - @recorder.log).join("\n\n")
+ "Extra queries: \n\n #{extra_queries}"
+ else
+ @recorder.log_message
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
new file mode 100644
index 00000000000..221926aaf7e
--- /dev/null
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -0,0 +1,28 @@
+shared_examples 'comment on merge request file' do
+ it 'adds a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.notes_holder') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ visit(merge_request_path(merge_request))
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
new file mode 100644
index 00000000000..c92c7f603d6
--- /dev/null
+++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
@@ -0,0 +1,21 @@
+shared_examples 'issuable user dropdown behaviors' do
+ include FilteredSearchHelpers
+
+ before do
+ issuable # ensure we have at least one issuable
+ sign_in(user_in_dropdown)
+ end
+
+ %w[author assignee].each do |dropdown|
+ describe "#{dropdown} dropdown", :js do
+ it 'only includes members of the project/group' do
+ visit issuables_path
+
+ filtered_search.set("#{dropdown}:")
+
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
+ end
+ end
+ end
+end
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 639b0924197..639b0924197 100644
--- a/spec/support/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
diff --git a/spec/support/shared_examples/features/search_shared_examples.rb b/spec/support/shared_examples/features/search_shared_examples.rb
new file mode 100644
index 00000000000..25ebbf011d5
--- /dev/null
+++ b/spec/support/shared_examples/features/search_shared_examples.rb
@@ -0,0 +1,5 @@
+shared_examples 'top right search form' do
+ it 'does not show top right search form' do
+ expect(page).not_to have_selector('.search')
+ end
+end
diff --git a/spec/support/shared_examples/position_formatters.rb b/spec/support/shared_examples/position_formatters.rb
new file mode 100644
index 00000000000..ffc9456dbc7
--- /dev/null
+++ b/spec/support/shared_examples/position_formatters.rb
@@ -0,0 +1,43 @@
+shared_examples_for "position formatter" do
+ let(:formatter) { described_class.new(attrs) }
+
+ describe '#key' do
+ let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] }
+
+ subject { formatter.key }
+
+ it { is_expected.to eq(key) }
+ end
+
+ describe '#complete?' do
+ subject { formatter.complete? }
+
+ context 'when there are missing key attributes' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when old_line and new_line are nil' do
+ let(:attrs) { base_attrs }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#to_h' do
+ let(:formatter_hash) do
+ attrs.merge(position_type: base_attrs[:position_type] || 'text' )
+ end
+
+ subject { formatter.to_h }
+
+ it { is_expected.to eq(formatter_hash) }
+ end
+
+ describe '#==' do
+ subject { formatter }
+
+ let(:other_formatter) { described_class.new(attrs) }
+
+ it { is_expected.to eq(other_formatter) }
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
new file mode 100644
index 00000000000..c9302f7b750
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -0,0 +1,103 @@
+shared_examples 'custom attributes endpoints' do |attributable_name|
+ let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' }
+ let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
+
+ describe "GET /#{attributable_name} with custom attributes filter" do
+ let!(:other_attributable) { create attributable.class.name.underscore }
+
+ context 'with an unauthorized user' do
+ it 'does not filter by custom attributes' do
+ get api("/#{attributable_name}", user), custom_attributes: { foo: 'foo', bar: 'bar' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to be 2
+ end
+ end
+
+ it 'filters by custom attributes' do
+ get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to be 1
+ expect(json_response.first['id']).to eq attributable.id
+ end
+ end
+
+ describe "GET /#{attributable_name}/:id/custom_attributes" do
+ context 'with an unauthorized user' do
+ subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes", user) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it 'returns all custom attributes' do
+ get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to contain_exactly(
+ { 'key' => 'foo', 'value' => 'foo' },
+ { 'key' => 'bar', 'value' => 'bar' }
+ )
+ end
+ end
+
+ describe "GET /#{attributable_name}/:id/custom_attributes/:key" do
+ context 'with an unauthorized user' do
+ subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it 'returns a single custom attribute' do
+ get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' })
+ end
+ end
+
+ describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do
+ context 'with an unauthorized user' do
+ subject { put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user), value: 'new' }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it 'creates a new custom attribute' do
+ expect do
+ put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new'
+ end.to change { attributable.custom_attributes.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' })
+ expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new'
+ end
+
+ it 'updates an existing custom attribute' do
+ expect do
+ put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new'
+ end.not_to change { attributable.custom_attributes.count }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' })
+ expect(custom_attribute1.reload.value).to eq 'new'
+ end
+ end
+
+ describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do
+ context 'with an unauthorized user' do
+ subject { delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it 'deletes an existing custom attribute' do
+ expect do
+ delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
+ end.to change { attributable.custom_attributes.count }.by(-1)
+
+ expect(response).to have_http_status(204)
+ expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil
+ end
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 45c10e78789..2dfb4d4a07f 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -42,7 +42,7 @@ module StubConfiguration
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
messages.each do |storage_name, storage_settings|
- storage_settings['path'] ||= TestEnv.repos_path
+ storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path')
storage_settings['failure_count_threshold'] ||= 10
storage_settings['failure_wait_time'] ||= 30
storage_settings['failure_reset_time'] ||= 1800
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 78a2ff73746..5f22d886910 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -39,11 +39,11 @@ module StubGitlabCalls
.and_return({ 'tags' => tags })
allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:repository_manifest).with(repository)
+ .to receive(:repository_manifest).with(repository, anything)
.and_return(stub_container_registry_tag_manifest)
allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:blob).with(repository)
+ .to receive(:blob).with(repository, anything, 'application/octet-stream')
.and_return(stub_container_registry_blob)
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 71b9deeabc3..a27bfdee3d2 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -3,6 +3,8 @@ require 'rspec/mocks'
module TestEnv
extend self
+ ComponentFailedToInstallError = Class.new(StandardError)
+
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'signed-commits' => '2d1096e',
@@ -15,6 +17,7 @@ module TestEnv
'feature_conflict' => 'bb5206f',
'fix' => '48f0be4',
'improve/awesome' => '5937ac0',
+ 'merged-target' => '21751bf',
'markdown' => '0ed8c6c',
'lfs' => 'be93687',
'master' => 'b83d6e3',
@@ -43,7 +46,8 @@ module TestEnv
'v1.1.0' => 'b83d6e3',
'add-ipython-files' => '93ee732',
'add-pdf-file' => 'e774ebd',
- 'add-pdf-text-binary' => '79faa7b'
+ 'add-pdf-text-binary' => '79faa7b',
+ 'add_images_and_changes' => '010d106'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -63,6 +67,11 @@ module TestEnv
# See gitlab.yml.example test section for paths
#
def init(opts = {})
+ unless Rails.env.test?
+ puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
+ exit 1
+ end
+
# Disable mailer for spinach tests
disable_mailer if opts[:mailer] == false
@@ -122,50 +131,23 @@ module TestEnv
end
def setup_gitlab_shell
- puts "\n==> Setting up Gitlab Shell..."
- start = Time.now
- gitlab_shell_dir = Gitlab.config.gitlab_shell.path
- shell_needs_update = component_needs_update?(gitlab_shell_dir,
- Gitlab::Shell.version_required)
-
- unless !shell_needs_update || system('rake', 'gitlab:shell:install')
- puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n"
- FileUtils.rm_rf(gitlab_shell_dir)
- exit 1
- end
-
- puts " GitLab Shell setup in #{Time.now - start} seconds...\n"
+ component_timed_setup('GitLab Shell',
+ install_dir: Gitlab.config.gitlab_shell.path,
+ version: Gitlab::Shell.version_required,
+ task: 'gitlab:shell:install')
end
def setup_gitaly
- puts "\n==> Setting up Gitaly..."
- start = Time.now
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
- if gitaly_dir_stale?(gitaly_dir)
- puts " Gitaly is outdated, cleaning up #{gitaly_dir}!"
- FileUtils.rm_rf(gitaly_dir)
- end
-
- gitaly_needs_update = component_needs_update?(gitaly_dir,
- Gitlab::GitalyClient.expected_server_version)
+ component_timed_setup('Gitaly',
+ install_dir: gitaly_dir,
+ version: Gitlab::GitalyClient.expected_server_version,
+ task: "gitlab:gitaly:install[#{gitaly_dir}]") do
- unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
- puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n"
- FileUtils.rm_rf(gitaly_dir)
- exit 1
+ start_gitaly(gitaly_dir)
end
-
- start_gitaly(gitaly_dir)
- puts " Gitaly setup in #{Time.now - start} seconds...\n"
- end
-
- def gitaly_dir_stale?(dir)
- gitaly_executable = File.join(dir, 'gitaly')
- return false unless File.exist?(gitaly_executable)
-
- File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION'))
end
def start_gitaly(gitaly_dir)
@@ -320,6 +302,43 @@ module TestEnv
end
end
+ def component_timed_setup(component, install_dir:, version:, task:)
+ puts "\n==> Setting up #{component}..."
+ start = Time.now
+
+ ensure_component_dir_name_is_correct!(component, install_dir)
+
+ # On CI, once installed, components never need update
+ return if File.exist?(install_dir) && ENV['CI']
+
+ if component_needs_update?(install_dir, version)
+ # Cleanup the component entirely to ensure we start fresh
+ FileUtils.rm_rf(install_dir)
+ unless system('rake', task)
+ raise ComponentFailedToInstallError
+ end
+ end
+
+ yield if block_given?
+
+ rescue ComponentFailedToInstallError
+ puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
+ FileUtils.rm_rf(install_dir)
+ exit 1
+ ensure
+ puts " #{component} setup in #{Time.now - start} seconds...\n"
+ end
+
+ def ensure_component_dir_name_is_correct!(component, path)
+ actual_component_dir_name = File.basename(path)
+ expected_component_dir_name = component.parameterize
+
+ unless actual_component_dir_name == expected_component_dir_name
+ puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
+ exit 1
+ end
+ end
+
def component_needs_update?(component_folder, expected_version)
version = File.read(File.join(component_folder, 'VERSION')).strip
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
index 1490287681b..50a1d4a56e2 100644
--- a/spec/support/update_invalid_issuable.rb
+++ b/spec/support/update_invalid_issuable.rb
@@ -25,11 +25,13 @@ shared_examples 'update invalid issuable' do |klass|
.and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
- it 'renders edit when format is html' do
- put :update, params
+ if klass == MergeRequest
+ it 'renders edit when format is html' do
+ put :update, params
- expect(response).to render_template(:edit)
- expect(assigns[:conflict]).to be_truthy
+ expect(response).to render_template(:edit)
+ expect(assigns[:conflict]).to be_truthy
+ end
end
it 'renders json error message when format is json' do
@@ -42,16 +44,17 @@ shared_examples 'update invalid issuable' do |klass|
end
end
- context 'when updating an invalid issuable' do
- before do
- key = klass == Issue ? :issue : :merge_request
- params[key][:title] = ""
- end
+ if klass == MergeRequest
+ context 'when updating an invalid issuable' do
+ before do
+ params[:merge_request][:title] = ""
+ end
- it 'renders edit when merge request is invalid' do
- put :update, params
+ it 'renders edit when merge request is invalid' do
+ put :update, params
- expect(response).to render_template(:edit)
+ expect(response).to render_template(:edit)
+ end
end
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 0c8c8a2ab05..886052d7848 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -224,17 +224,20 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
+ let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storages) do
+ {
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
+ 'test_second_storage' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
+ }
+ end
+
let(:project_a) { create(:project, :repository, repository_storage: 'default') }
- let(:project_b) { create(:project, :repository, repository_storage: 'custom') }
+ let(:project_b) { create(:project, :repository, repository_storage: 'test_second_storage') }
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
- gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address
- storages = {
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
- 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
- }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
# Create the projects now, after mocking the settings but before doing the backup
@@ -253,7 +256,7 @@ describe 'gitlab:app namespace rake task' do
after do
FileUtils.rm_rf('tmp/tests/default_storage')
FileUtils.rm_rf('tmp/tests/custom_storage')
- FileUtils.rm(@backup_tar)
+ FileUtils.rm(@backup_tar) if @backup_tar
end
it 'includes repositories in all repository storages' do
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
new file mode 100644
index 00000000000..f59792c3d36
--- /dev/null
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -0,0 +1,52 @@
+require 'rake_helper'
+
+describe 'gitlab:storage rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/storage'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ describe 'migrate_to_hashed rake task' do
+ context '0 legacy projects' do
+ it 'does nothing' do
+ expect(StorageMigratorWorker).not_to receive(:perform_async)
+
+ run_rake_task('gitlab:storage:migrate_to_hashed')
+ end
+ end
+
+ context '5 legacy projects' do
+ let(:projects) { create_list(:project, 5, storage_version: 0) }
+
+ context 'in batches of 1' do
+ before do
+ stub_env('BATCH' => 1)
+ end
+
+ it 'enqueues one StorageMigratorWorker per project' do
+ projects.each do |project|
+ expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id)
+ end
+
+ run_rake_task('gitlab:storage:migrate_to_hashed')
+ end
+ end
+
+ context 'in batches of 2' do
+ before do
+ stub_env('BATCH' => 2)
+ end
+
+ it 'enqueues one StorageMigratorWorker per 2 projects' do
+ projects.map(&:id).sort.each_slice(2) do |first, last|
+ last ||= first
+ expect(StorageMigratorWorker).to receive(:perform_async).with(first, last)
+ end
+
+ run_rake_task('gitlab:storage:migrate_to_hashed')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index d34617be474..fae5ec35c47 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -75,4 +75,24 @@ describe Gitlab::TaskHelpers do
subject.checkout_version(tag, clone_path)
end
end
+
+ describe '#run_command' do
+ it 'runs command and return the output' do
+ expect(subject.run_command(%w(echo it works!))).to eq("it works!\n")
+ end
+
+ it 'returns empty string when command doesnt exist' do
+ expect(subject.run_command(%w(nonexistentcommand with arguments))).to eq('')
+ end
+ end
+
+ describe '#run_command!' do
+ it 'runs command and return the output' do
+ expect(subject.run_command!(%w(echo it works!))).to eq("it works!\n")
+ end
+
+ it 'returns and exception when command exit with non zero code' do
+ expect { subject.run_command!(['bash', '-c', 'exit 1']) }.to raise_error Gitlab::TaskFailedError
+ end
+ end
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
index f2c19c7642a..7724d54c569 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'ci/lints/show' do
include Devise::Test::ControllerHelpers
describe 'XSS protection' do
- let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do
assign(:status, true)
assign(:builds, config_processor.builds)
@@ -59,7 +59,7 @@ describe 'ci/lints/show' do
}
end
- let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
context 'when the content is valid' do
before do
diff --git a/spec/views/dashboard/projects/_nav.html.haml.rb b/spec/views/dashboard/projects/_nav.html.haml.rb
new file mode 100644
index 00000000000..f6a8ca13040
--- /dev/null
+++ b/spec/views/dashboard/projects/_nav.html.haml.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe 'dashboard/projects/_nav.html.haml' do
+ it 'highlights All tab by default' do
+ render
+
+ expect(rendered).to have_css('li.active a', text: 'All')
+ end
+
+ it 'highlights Personal tab personal param is present' do
+ controller.params[:personal] = true
+
+ render
+
+ expect(rendered).to have_css('li.active a', text: 'Personal')
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index b17bc6692f3..c5f455b8948 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -1,16 +1,28 @@
require 'spec_helper'
describe 'layouts/nav/sidebar/_project' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign(:project, project)
+ assign(:repository, project.repository)
+ allow(view).to receive(:current_ref).and_return('master')
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ describe 'issue boards' do
+ it 'has boards tab when multiple issue boards available' do
+ render
+
+ expect(rendered).to have_css('a[title="Board"]')
+ end
+ end
+
describe 'container registry tab' do
before do
- project = create(:project, :repository)
stub_container_registry_config(enabled: true)
- assign(:project, project)
- assign(:repository, project.repository)
- 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)
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index 98c7de9b709..efed2e02a1b 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe 'projects/merge_requests/_commits.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:target_project) { create(:project, :repository) }
- let(:source_project) { create(:project, :repository, forked_from_project: target_project) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { fork_project(target_project, user, repository: true) }
let(:merge_request) do
create(:merge_request, :simple,
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 69c7d0cbf28..9b74a7e1946 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -2,16 +2,19 @@ require 'spec_helper'
describe 'projects/merge_requests/edit.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
let(:milestone) { create(:milestone, project: project) }
let(:closed_merge_request) do
+ project.add_developer(user)
+
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
author: user,
assignee: user,
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 6f29d12373a..28d54c2fb77 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -2,16 +2,17 @@ require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
let(:closed_merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
author: user)
end
@@ -52,7 +53,7 @@ describe 'projects/merge_requests/show.html.haml' do
context 'when the merge request is open' do
it 'closes the merge request if the source project does not exist' do
closed_merge_request.update_attributes(state: 'open')
- fork_project.destroy
+ forked_project.destroy
render
diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
new file mode 100644
index 00000000000..c757ccf02d3
--- /dev/null
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe 'projects/pipelines_settings/_show' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign :project, project
+ end
+
+ context 'when kubernetes is not active' do
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the')
+ expect(rendered).to have_link('Kubernetes service')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the')
+ expect(rendered).to have_link('Kubernetes service')
+ end
+ end
+ end
+
+ context 'when kubernetes is active' do
+ before do
+ project.build_kubernetes_service(active: true)
+ end
+
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'does not show warning message' do
+ render
+
+ expect(rendered).not_to have_css('.settings-message')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
deleted file mode 100644
index cf0aa44a4a2..00000000000
--- a/spec/views/projects/registry/repositories/index.html.haml_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/registry/repositories/index' do
- let(:group) { create(:group, path: 'group') }
- let(:project) { create(:project, group: group, path: 'test') }
-
- let(:repository) do
- create(:container_repository, project: project, name: 'image')
- end
-
- before do
- stub_container_registry_config(enabled: true,
- host_port: 'registry.gitlab',
- api_url: 'http://registry.gitlab')
-
- stub_container_registry_tags(repository: :any, tags: [:latest])
-
- assign(:project, project)
- assign(:images, [repository])
-
- allow(view).to receive(:can?).and_return(true)
- end
-
- it 'contains container repository path' do
- render
-
- expect(rendered).to have_content 'group/test/image'
- end
-
- it 'contains attribute for copying tag location into clipboard' do
- render
-
- expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \
- 'registry.gitlab/group/test/image:latest"]'
- end
-end
diff --git a/spec/views/shared/issuable/_participants.html.haml.rb b/spec/views/shared/issuable/_participants.html.haml.rb
new file mode 100644
index 00000000000..51059d4c0d7
--- /dev/null
+++ b/spec/views/shared/issuable/_participants.html.haml.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'nokogiri'
+
+describe 'shared/issuable/_participants.html.haml' do
+ let(:project) { create(:project) }
+ let(:participants) { create_list(:user, 100) }
+
+ before do
+ allow(view).to receive_messages(project: project,
+ participants: participants)
+ end
+
+ it 'renders lazy loaded avatars' do
+ render 'shared/issuable/participants'
+
+ html = Nokogiri::HTML(rendered)
+
+ avatars = html.css('.participants-author img')
+
+ avatars.each do |avatar|
+ expect(avatar[:class]).to include('lazy')
+ expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
+ expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
+ end
+ end
+end
diff --git a/spec/views/shared/milestones/_issuable.html.haml.rb b/spec/views/shared/milestones/_issuable.html.haml.rb
new file mode 100644
index 00000000000..0a3f877cae0
--- /dev/null
+++ b/spec/views/shared/milestones/_issuable.html.haml.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_issuable.html.haml' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:issuable) { create(:issue, project: project, assignees: [user]) }
+
+ before do
+ assign(:project, project)
+ assign(:milestone, milestone)
+ end
+
+ it 'avatar links to issues page' do
+ render 'shared/milestones/issuable', issuable: issuable, show_project_name: true
+
+ expect(rendered).to have_css("a[href='#{project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id, state: 'all')}']")
+ end
+end
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 8cc3f37ebe8..1a7ffd5cdbf 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -11,6 +11,8 @@ describe BuildFinishedWorker do
expect(BuildHooksWorker)
.to receive(:new).ordered.and_call_original
+ expect(BuildTraceSectionsWorker)
+ .to receive(:perform_async)
expect_any_instance_of(BuildCoverageWorker)
.to receive(:perform)
expect_any_instance_of(BuildHooksWorker)
diff --git a/spec/workers/build_trace_sections_worker_spec.rb b/spec/workers/build_trace_sections_worker_spec.rb
new file mode 100644
index 00000000000..45243f45547
--- /dev/null
+++ b/spec/workers/build_trace_sections_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildTraceSectionsWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build) }
+
+ it 'updates trace sections' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:parse_trace_sections!)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
new file mode 100644
index 00000000000..11f208289db
--- /dev/null
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ClusterProvisionWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+
+ described_class.new.perform(123)
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
new file mode 100644
index 00000000000..1050651fa51
--- /dev/null
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ClusterQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include ClusterQueue
+ end
+ end
+
+ it 'sets a default pipelines queue automatically' do
+ expect(worker.sidekiq_options['queue'])
+ .to eq :gcp_cluster
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 6f9ddb6c63c..47297de738b 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -31,7 +31,7 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id, :gc, lease_key, lease_uuid)
@@ -47,7 +47,6 @@ describe GitGarbageCollectWorker do
expect(subject).not_to receive(:command)
expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
subject.perform(project.id, :gc, lease_key, lease_uuid)
@@ -77,7 +76,7 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id)
@@ -93,7 +92,6 @@ describe GitGarbageCollectWorker do
expect(subject).not_to receive(:command)
expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
subject.perform(project.id)
@@ -106,7 +104,7 @@ describe GitGarbageCollectWorker do
it_should_behave_like 'flushing ref caches', true
end
- context "with Gitaly turned off", skip_gitaly_mock: true do
+ context "with Gitaly turned off", :skip_gitaly_mock do
it_should_behave_like 'flushing ref caches', false
end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 20cf580af8a..ed8cedc0079 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe NamespacelessProjectDestroyWorker do
+ include ProjectForksHelper
+
subject { described_class.new }
before do
@@ -55,9 +57,11 @@ describe NamespacelessProjectDestroyWorker do
context 'project forked from another' do
let!(:parent_project) { create(:project) }
-
- before do
- create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project)
+ let(:project) do
+ namespaceless_project = fork_project(parent_project)
+ namespaceless_project.namespace_id = nil
+ namespaceless_project.save(validate: false)
+ namespaceless_project
end
it 'closes open merge requests' do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index d3707a3cc11..05eecf5f0bb 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -70,12 +70,15 @@ describe PostReceive do
context "creates a Ci::Pipeline for every change" do
before do
- allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
- OpenStruct.new(id: '123456')
- end
- allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
- allow_any_instance_of(Repository).to receive(:ref_exists?).and_return(true)
stub_ci_pipeline_to_return_yaml_file
+
+ # TODO, don't stub private methods
+ #
+ allow_any_instance_of(Ci::CreatePipelineService)
+ .to receive(:commit).and_return(OpenStruct.new(id: '123456'))
+
+ allow_any_instance_of(Repository)
+ .to receive(:branch_exists?).and_return(true)
end
it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
new file mode 100644
index 00000000000..f5226dee0ad
--- /dev/null
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe ProjectMigrateHashedStorageWorker do
+ describe '#perform' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) }
+
+ it 'skips when project no longer exists' do
+ nonexistent_id = 999999999999
+
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ subject.perform(nonexistent_id)
+ end
+
+ it 'skips when project is pending delete' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+
+ subject.perform(pending_delete_project.id)
+ end
+
+ it 'delegates removal to service class' do
+ service = double('service')
+ expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service)
+ expect(service).to receive(:execute)
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index d2609d21546..1d9bbf2ca62 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -69,7 +69,12 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
def break_wiki(project)
- FileUtils.rm_rf(wiki_path(project) + '/objects')
+ objects_dir = wiki_path(project) + '/objects'
+
+ # Replace the /objects directory with a file so that the repo is
+ # invalid, _and_ 'git init' cannot fix it.
+ FileUtils.rm_rf(objects_dir)
+ FileUtils.touch(objects_dir) if File.directory?(wiki_path(project))
end
def wiki_path(project)
diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb
new file mode 100644
index 00000000000..8619ff2f7da
--- /dev/null
+++ b/spec/workers/storage_migrator_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe StorageMigratorWorker do
+ subject(:worker) { described_class.new }
+ let(:projects) { create_list(:project, 2) }
+
+ describe '#perform' do
+ let(:ids) { projects.map(&:id) }
+
+ it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
+ expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
+
+ worker.perform(ids.min, ids.max)
+ end
+
+ it 'sets projects as read only' do
+ allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
+ worker.perform(ids.min, ids.max)
+
+ projects.each do |project|
+ expect(project.reload.repository_read_only?).to be_truthy
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
+ expect { worker.perform(ids.min, ids.max) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb
deleted file mode 100644
index e50c788b82a..00000000000
--- a/spec/workers/use_key_worker_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require 'spec_helper'
-
-describe UseKeyWorker do
- describe "#perform" do
- it "updates the key's last_used_at attribute to the current time when it exists" do
- worker = described_class.new
- key = create(:key)
- current_time = Time.zone.now
-
- Timecop.freeze(current_time) do
- expect { worker.perform(key.id) }
- .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time)
- end
- end
-
- it "returns false and skips the job when the key doesn't exist" do
- worker = described_class.new
- key = create(:key)
-
- expect(worker.perform(key.id + 1)).to eq false
- end
- end
-end
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
new file mode 100644
index 00000000000..dcd4a3b9aec
--- /dev/null
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe WaitForClusterCreationWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ before do
+ allow(operation).to receive(:status).and_return(status)
+ allow(operation).to receive(:start_time).and_return(1.minute.ago)
+ allow(operation).to receive(:status_message).and_return('error')
+ allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ let(:status) { 'RUNNING' }
+
+ it 'reschedules worker' do
+ expect(described_class).to receive(:perform_in)
+
+ described_class.new.perform(cluster.id)
+ end
+
+ context 'when operation timeout' do
+ before do
+ allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
+ end
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when operation status is DONE' do
+ let(:status) { 'DONE' }
+
+ it 'finalizes cluster creation' do
+ expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when operation status is others' do
+ let(:status) { 'others' }
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+
+ described_class.new.perform(1234)
+ end
+ end
+ end
+end
diff --git a/symbol/icons.svg b/symbol/icons.svg
new file mode 100644
index 00000000000..433bd2aca0d
--- /dev/null
+++ b/symbol/icons.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 1792 1792" id="clock_o" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V544q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-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.5T1281.5 1561 896 1664t-385.5-103T231 1281.5 128 896t103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z"/></symbol><symbol viewBox="0 0 36 18" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7H2C.9 7 0 7.9 0 9s.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7H34c1.1 0 2-.9 2-2s-.9-2-2-2m-16 7c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol></svg> \ No newline at end of file
diff --git a/symbol/sprite.symbol.html b/symbol/sprite.symbol.html
new file mode 100644
index 00000000000..a2289014093
--- /dev/null
+++ b/symbol/sprite.symbol.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
+ <script src="https://rawgit.com/jonathantneal/svg4everybody/master/dist/svg4everybody.js"></script>
+ <script>svg4everybody();</script>
+ <title>SVG &lt;symbol&gt; sprite preview | svg-sprite</title>
+ <style>@charset "UTF-8";body{padding:0;margin:0;color:#666;background:#fafafa;font-family:Arial,Helvetica,sans-serif;font-size:1em;line-height:1.4}header{display:block;padding:3em 3em 2em 3em;background-color:#fff}header p{margin:2em 0 0 0}section{border-top:1px solid #eee;padding:2em 3em 0 3em}section ul{margin:0;padding:0}section li{display:inline;display:inline-block;background-color:#fff;position:relative;margin:0 2em 2em 0;vertical-align:top;border:1px solid #ccc;padding:1em 1em 3em 1em;cursor:default}.icon-box{margin:0;width:144px;height:144px;position:relative;background:#ccc url("data:image/gif;base64,R0lGODlhDAAMAIAAAMzMzP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozQjk4OTI0MUY5NTIxMUUyQkJDMEI5NEFEM0Y1QTYwQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozQjk4OTI0MkY5NTIxMUUyQkJDMEI5NEFEM0Y1QTYwQyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjNCOTg5MjNGRjk1MjExRTJCQkMwQjk0QUQzRjVBNjBDIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjNCOTg5MjQwRjk1MjExRTJCQkMwQjk0QUQzRjVBNjBDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAAwADAAAAhaEH6mHmmzcgzJAUG/NVGrfOZ8YLlABADs=") top left repeat;border:1px solid #ccc;display:table-cell;vertical-align:middle;text-align:center}.icon{display:inline;display:inline-block}h1{margin-top:0}h2{margin:0;padding:0;font-size:1em;font-weight:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:absolute;left:1em;right:1em;bottom:1em}footer{display:block;margin:0;padding:0 3em 3em 3em}footer p{margin:0;font-size:.7em}footer a{color:#0f7595;margin-left:0}</style>
+
+<!--
+
+Sprite shape dimensions
+====================================================================================================
+You will need to set the sprite shape dimensions via CSS when you use them as inline SVG, otherwise
+they would become a huge 100% in size. You may use the following dimension classes for doing so.
+They might well be outsourced to an external stylesheet of course.
+
+-->
+
+<style type="text/css">
+ .svg-clock_o-dims { width: 200px; height: 200px; }
+ .svg-commit-dims { width: 36px; height: 18px; }
+ .svg-project-dims { width: 16px; height: 16px; }
+</style>
+<!--
+====================================================================================================
+-->
+
+ </head>
+ <body>
+
+<!--
+
+Inline <symbol> SVG sprite
+====================================================================================================
+This is an inlined version of the generated SVG sprite. The single images may be <use>d everywhere
+below within this document. Please see
+
+ https://github.com/jkphl/svg-sprite/blob/master/docs/configuration.md#defs--symbol-mode
+
+for further details on how to create this embeddable sprite variant.
+
+-->
+
+<svg width="0" height="0" style="position:absolute">
+ <symbol viewBox="0 0 1792 1792" id="clock_o" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V544q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-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.5T1281.5 1561 896 1664t-385.5-103T231 1281.5 128 896t103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z"/></symbol>
+ <symbol viewBox="0 0 36 18" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7H2C.9 7 0 7.9 0 9s.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7H34c1.1 0 2-.9 2-2s-.9-2-2-2m-16 7c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5"/></symbol>
+ <symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol>
+</svg>
+
+<!--
+====================================================================================================
+-->
+
+ <header>
+ <h1>SVG <code>&lt;symbol&gt;</code> sprite preview</h1>
+ <p>This preview features two methods of using the generated sprite in conjunction with inline SVG. Please have a look at the HTML source for further details and be aware of the following constraints:</p>
+ <ul>
+ <li>Your browser has to <a href="http://caniuse.com/#feat=svg-html5" target="_blank">support inline SVG</a> for these techniques to work.</li>
+ <li>The embedded sprite (A) slightly differs from the generated external one. Please <a href="https://github.com/jkphl/svg-sprite/blob/master/docs/configuration.md#defs--symbol-mode" target="_blank">see the documentation</a> for details on how to create such an embeddable sprite.</li>
+ <li>Internet Explorer up to version 11 doesn't support external sprites for use with inline SVG. For IE 9-11, you may polyfill this functionality with <a href="https://github.com/jonathantneal/svg4everybody" target="_blank">SVG for Everybody</a>.</li>
+ </ul>
+ </header>
+ <section>
+
+<!--
+
+A) Inline SVG with embedded sprite
+====================================================================================================
+These SVG images make use of fragment identifiers (IDs) and are extracted out of the inline sprite
+embedded above. They may be styled via CSS.
+
+-->
+
+ <h3>A) Inline SVG with embedded sprite</h3>
+ <ul>
+
+ <li title="clock_o">
+ <div class="icon-box">
+
+ <!-- clock_o -->
+ <svg class="svg-clock_o-dims">
+ <use xlink:href="#clock_o"></use>
+ </svg>
+
+ </div>
+ <h2>clock_o</h2>
+ </li>
+ <li title="commit">
+ <div class="icon-box">
+
+ <!-- commit -->
+ <svg class="svg-commit-dims">
+ <use xlink:href="#commit"></use>
+ </svg>
+
+ </div>
+ <h2>commit</h2>
+ </li>
+ <li title="project">
+ <div class="icon-box">
+
+ <!-- project -->
+ <svg class="svg-project-dims">
+ <use xlink:href="#project"></use>
+ </svg>
+
+ </div>
+ <h2>project</h2>
+ </li>
+ </ul>
+
+<!--
+====================================================================================================
+-->
+
+ </section>
+ <section>
+
+<!--
+
+B) Inline SVG with external sprite (IE 9-11 with polyfill only)
+====================================================================================================
+These SVG images make use of an URL + fragment identifiers (IDs) and refer to the regular external
+SVG sprite. They may be styled via CSS. (IE 9-11 with polyfill only)
+
+-->
+
+ <h3>B) Inline SVG with external sprite (IE 9-11 with polyfill only)</h3>
+ <ul>
+
+ <li title="clock_o">
+ <div class="icon-box">
+
+ <!-- clock_o -->
+ <svg class="svg-clock_o-dims">
+ <use xlink:href="icons.svg#clock_o"></use>
+ </svg>
+
+ </div>
+ <h2>clock_o</h2>
+ </li>
+ <li title="commit">
+ <div class="icon-box">
+
+ <!-- commit -->
+ <svg class="svg-commit-dims">
+ <use xlink:href="icons.svg#commit"></use>
+ </svg>
+
+ </div>
+ <h2>commit</h2>
+ </li>
+ <li title="project">
+ <div class="icon-box">
+
+ <!-- project -->
+ <svg class="svg-project-dims">
+ <use xlink:href="icons.svg#project"></use>
+ </svg>
+
+ </div>
+ <h2>project</h2>
+ </li>
+ </ul>
+
+<!--
+====================================================================================================
+-->
+
+ </section>
+ <footer>
+ <p>Generated at Fri, 25 Aug 2017 12:38:01 GMT by <a href="https://github.com/jkphl/svg-sprite" target="_blank">svg-sprite</a>.</p>
+ </footer>
+ </body>
+</html>
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index a07edd264c3..c93e6567baf 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -271,10 +271,6 @@ production:
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
chart/
-
- if [[ "$track" == "stable" ]]; then
- kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/${CI_ENVIRONMENT_SLUG}-auto-deploy"
- fi
}
function install_dependencies() {
diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md
index 6e5160a2487..d4c057bf9dc 100644
--- a/vendor/gitlab-ci-yml/CONTRIBUTING.md
+++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md
@@ -1,5 +1,47 @@
-The canonical repository for `.gitlab-ci.yml` templates is
-https://gitlab.com/gitlab-org/gitlab-ci-yml.
+## Contributing
+
+Thank you for your interest in contributing to this GitLab project! We welcome
+all contributions. By participating in this project, you agree to abide by the
+[code of conduct](#code-of-conduct).
+
+## Contributor license agreement
+
+By submitting code as an individual you agree to the [individual contributor
+license agreement][individual-agreement].
+
+By submitting code as an entity you agree to the [corporate contributor license
+agreement][corporate-agreement].
+
+## Code of conduct
+
+As contributors and maintainers of this project, we pledge to respect all people
+who contribute through reporting issues, posting feature requests, updating
+documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free
+experience for everyone, regardless of level of experience, gender, gender
+identity and expression, sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, or religion.
+
+Examples of unacceptable behavior by participants include the use of sexual
+language or imagery, derogatory comments or personal attacks, trolling, public
+or private harassment, insults, or other unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct. Project maintainers who do not follow the
+Code of Conduct may be removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior can be
+reported by emailing contact@gitlab.com.
+
+This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
+available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
+
+[contributor-covenant]: http://contributor-covenant.org
+[individual-agreement]: https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html
+[corporate-agreement]: https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html
-GitLab only mirrors the templates. Please submit your merge requests to
-https://gitlab.com/gitlab-org/gitlab-ci-yml.
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
index 06b0c84e516..6e5fe97cf6d 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
@@ -1,3 +1,6 @@
+# This template has been DEPRECATED. Consider using Auto DevOps instead:
+# https://docs.gitlab.com/ee/topics/autodevops
+
# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index 722934b7981..019a4d4cd7d 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -1,3 +1,6 @@
+# This template has been DEPRECATED. Consider using Auto DevOps instead:
+# https://docs.gitlab.com/ee/topics/autodevops
+
# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
index acba718ebe4..60a9430a839 100644
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -1,3 +1,6 @@
+# This template has been DEPRECATED. Consider using Auto DevOps instead:
+# https://docs.gitlab.com/ee/topics/autodevops
+
# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/openshift-deploy
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 5beb3e5e9bf..24623ff4c1f 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,5 +1,5 @@
RedCloth,4.3.2,MIT
-abbrev,1.1.0,ISC
+abbrev,1.0.9,ISC
accepts,1.3.3,MIT
ace-rails-ap,4.1.2,MIT
acorn,5.1.1,MIT
@@ -15,9 +15,8 @@ activesupport,4.2.8,MIT
acts-as-taggable-on,4.0.0,MIT
addressable,2.3.8,Apache 2.0
after,0.8.2,MIT
-after_commit_queue,1.3.0,MIT
-ajv,4.11.8,MIT
-ajv-keywords,1.5.1,MIT
+ajv,5.2.0,MIT
+ajv-keywords,2.1.0,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
allocations,1.0.5,MIT
@@ -26,15 +25,15 @@ amdefine,1.0.1,BSD-3-Clause OR MIT
ansi-escapes,1.4.0,MIT
ansi-html,0.0.5,"Apache, Version 2.0"
ansi-regex,2.1.1,MIT
-ansi-styles,3.1.0,MIT
-anymatch,1.3.0,ISC
+ansi-styles,2.2.1,MIT
+anymatch,1.3.2,ISC
append-transform,0.4.0,MIT
-aproba,1.1.2,ISC
+aproba,1.1.1,ISC
are-we-there-yet,1.1.4,ISC
arel,6.0.4,MIT
argparse,1.0.9,MIT
arr-diff,2.0.0,MIT
-arr-flatten,1.1.0,MIT
+arr-flatten,1.0.1,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
@@ -51,7 +50,7 @@ asn1,0.2.3,MIT
asn1.js,4.9.1,MIT
assert,1.4.1,MIT
assert-plus,0.2.0,MIT
-async,0.2.10,MIT
+async,2.4.1,MIT
async-each,1.0.1,MIT
asynckit,0.4.0,MIT
atomic,1.1.99,Apache 2.0
@@ -63,28 +62,29 @@ autoprefixer-rails,6.2.3,MIT
aws-sign2,0.6.0,Apache 2.0
aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
+axios,0.16.2,MIT
babel-code-frame,6.22.0,MIT
-babel-core,6.25.0,MIT
-babel-eslint,7.2.3,MIT
-babel-generator,6.25.0,MIT
-babel-helper-bindify-decorators,6.24.1,MIT
-babel-helper-builder-binary-assignment-operator-visitor,6.24.1,MIT
-babel-helper-call-delegate,6.24.1,MIT
-babel-helper-define-map,6.24.1,MIT
-babel-helper-explode-assignable-expression,6.24.1,MIT
-babel-helper-explode-class,6.24.1,MIT
-babel-helper-function-name,6.24.1,MIT
-babel-helper-get-function-arity,6.24.1,MIT
-babel-helper-hoist-variables,6.24.1,MIT
-babel-helper-optimise-call-expression,6.24.1,MIT
-babel-helper-regex,6.24.1,MIT
-babel-helper-remap-async-to-generator,6.24.1,MIT
-babel-helper-replace-supers,6.24.1,MIT
-babel-helpers,6.24.1,MIT
-babel-loader,6.4.1,MIT
+babel-core,6.23.1,MIT
+babel-eslint,7.2.1,MIT
+babel-generator,6.23.0,MIT
+babel-helper-bindify-decorators,6.22.0,MIT
+babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
+babel-helper-call-delegate,6.22.0,MIT
+babel-helper-define-map,6.23.0,MIT
+babel-helper-explode-assignable-expression,6.22.0,MIT
+babel-helper-explode-class,6.22.0,MIT
+babel-helper-function-name,6.23.0,MIT
+babel-helper-get-function-arity,6.22.0,MIT
+babel-helper-hoist-variables,6.22.0,MIT
+babel-helper-optimise-call-expression,6.23.0,MIT
+babel-helper-regex,6.22.0,MIT
+babel-helper-remap-async-to-generator,6.22.0,MIT
+babel-helper-replace-supers,6.23.0,MIT
+babel-helpers,6.23.0,MIT
+babel-loader,7.1.1,MIT
babel-messages,6.23.0,MIT
babel-plugin-check-es2015-constants,6.22.0,MIT
-babel-plugin-istanbul,4.1.4,New BSD
+babel-plugin-istanbul,4.0.0,New BSD
babel-plugin-syntax-async-functions,6.13.0,MIT
babel-plugin-syntax-async-generators,6.13.0,MIT
babel-plugin-syntax-class-properties,6.13.0,MIT
@@ -93,83 +93,82 @@ babel-plugin-syntax-dynamic-import,6.18.0,MIT
babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
babel-plugin-syntax-object-rest-spread,6.13.0,MIT
babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
-babel-plugin-transform-async-generator-functions,6.24.1,MIT
-babel-plugin-transform-async-to-generator,6.24.1,MIT
-babel-plugin-transform-class-properties,6.24.1,MIT
-babel-plugin-transform-decorators,6.24.1,MIT
-babel-plugin-transform-define,1.3.0,MIT
+babel-plugin-transform-async-generator-functions,6.22.0,MIT
+babel-plugin-transform-async-to-generator,6.22.0,MIT
+babel-plugin-transform-class-properties,6.23.0,MIT
+babel-plugin-transform-decorators,6.22.0,MIT
+babel-plugin-transform-define,1.2.0,MIT
babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
-babel-plugin-transform-es2015-block-scoping,6.24.1,MIT
-babel-plugin-transform-es2015-classes,6.24.1,MIT
-babel-plugin-transform-es2015-computed-properties,6.24.1,MIT
+babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
+babel-plugin-transform-es2015-classes,6.23.0,MIT
+babel-plugin-transform-es2015-computed-properties,6.22.0,MIT
babel-plugin-transform-es2015-destructuring,6.23.0,MIT
-babel-plugin-transform-es2015-duplicate-keys,6.24.1,MIT
+babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
babel-plugin-transform-es2015-for-of,6.23.0,MIT
-babel-plugin-transform-es2015-function-name,6.24.1,MIT
+babel-plugin-transform-es2015-function-name,6.22.0,MIT
babel-plugin-transform-es2015-literals,6.22.0,MIT
-babel-plugin-transform-es2015-modules-amd,6.24.1,MIT
-babel-plugin-transform-es2015-modules-commonjs,6.24.1,MIT
-babel-plugin-transform-es2015-modules-systemjs,6.24.1,MIT
-babel-plugin-transform-es2015-modules-umd,6.24.1,MIT
-babel-plugin-transform-es2015-object-super,6.24.1,MIT
-babel-plugin-transform-es2015-parameters,6.24.1,MIT
-babel-plugin-transform-es2015-shorthand-properties,6.24.1,MIT
+babel-plugin-transform-es2015-modules-amd,6.24.0,MIT
+babel-plugin-transform-es2015-modules-commonjs,6.24.0,MIT
+babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-umd,6.24.0,MIT
+babel-plugin-transform-es2015-object-super,6.22.0,MIT
+babel-plugin-transform-es2015-parameters,6.23.0,MIT
+babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
babel-plugin-transform-es2015-spread,6.22.0,MIT
-babel-plugin-transform-es2015-sticky-regex,6.24.1,MIT
+babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT
babel-plugin-transform-es2015-template-literals,6.22.0,MIT
babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
-babel-plugin-transform-es2015-unicode-regex,6.24.1,MIT
-babel-plugin-transform-exponentiation-operator,6.24.1,MIT
+babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT
+babel-plugin-transform-exponentiation-operator,6.22.0,MIT
babel-plugin-transform-object-rest-spread,6.23.0,MIT
-babel-plugin-transform-regenerator,6.24.1,MIT
-babel-plugin-transform-strict-mode,6.24.1,MIT
-babel-preset-es2015,6.24.1,MIT
-babel-preset-es2016,6.24.1,MIT
-babel-preset-es2017,6.24.1,MIT
-babel-preset-latest,6.24.1,MIT
-babel-preset-stage-2,6.24.1,MIT
-babel-preset-stage-3,6.24.1,MIT
-babel-register,6.24.1,MIT
-babel-runtime,6.23.0,MIT
-babel-template,6.25.0,MIT
-babel-traverse,6.25.0,MIT
-babel-types,6.25.0,MIT
+babel-plugin-transform-regenerator,6.22.0,MIT
+babel-plugin-transform-strict-mode,6.22.0,MIT
+babel-preset-es2015,6.24.0,MIT
+babel-preset-es2016,6.22.0,MIT
+babel-preset-es2017,6.22.0,MIT
+babel-preset-latest,6.24.0,MIT
+babel-preset-stage-2,6.22.0,MIT
+babel-preset-stage-3,6.22.0,MIT
+babel-register,6.23.0,MIT
+babel-runtime,6.22.0,MIT
+babel-template,6.23.0,MIT
+babel-traverse,6.23.1,MIT
+babel-types,6.23.0,MIT
babosa,1.0.2,MIT
-babylon,6.17.4,MIT
+babylon,6.16.1,MIT
backo2,1.0.2,MIT
balanced-match,1.0.0,MIT
base32,0.3.2,MIT
base64-arraybuffer,0.1.5,MIT
-base64-js,1.2.1,MIT
+base64-js,1.2.0,MIT
base64id,1.0.0,MIT
batch,0.6.1,MIT
bcrypt,3.1.11,MIT
bcrypt-pbkdf,1.0.1,New BSD
+bcrypt_pbkdf,1.0.0,MIT
better-assert,1.0.2,MIT
big.js,3.1.3,MIT
-binary-extensions,1.8.0,MIT
+binary-extensions,1.10.0,MIT
bindata,2.3.5,ruby
blob,0.0.4,unknown
block-stream,0.0.9,ISC
-bluebird,3.5.0,MIT
-bn.js,4.11.7,MIT
+bluebird,2.11.0,MIT
+bn.js,4.11.6,MIT
body-parser,1.17.2,MIT
bonjour,3.5.0,MIT
boom,2.10.1,New BSD
-bootsnap,1.1.1,MIT
bootstrap-sass,3.3.6,MIT
-bootstrap-sass,3.3.7,MIT
bootstrap_form,2.7.0,MIT
brace-expansion,1.1.8,MIT
braces,1.8.5,MIT
-brorand,1.1.0,MIT
+brorand,1.0.7,MIT
browser,2.2.0,MIT
browserify-aes,1.0.6,MIT
browserify-cipher,1.0.0,MIT
browserify-des,1.0.0,MIT
browserify-rsa,4.0.1,MIT
-browserify-sign,4.0.4,ISC
+browserify-sign,4.0.0,ISC
browserify-zlib,0.1.4,MIT
browserslist,1.7.7,MIT
buffer,4.9.1,MIT
@@ -183,36 +182,33 @@ bytes,2.4.0,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,unknown
callsites,0.2.0,MIT
-camelcase,1.2.1,MIT
+camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
-caniuse-db,1.0.30000699,CC-BY-4.0
+caniuse-db,1.0.30000649,CC-BY-4.0
carrierwave,1.1.0,MIT
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
chalk,1.1.3,MIT
-charlock_holmes,0.7.3,MIT
+charlock_holmes,0.7.5,MIT
chokidar,1.7.0,MIT
chronic,0.10.2,MIT
chronic_duration,0.10.6,MIT
chunky_png,1.3.5,MIT
-cipher-base,1.0.4,MIT
-circular-json,0.3.1,MIT
+cipher-base,1.0.3,MIT
+circular-json,0.3.3,MIT
citrus,3.0.2,MIT
-clap,1.2.0,MIT
+clap,1.1.3,MIT
cli-cursor,1.0.2,MIT
cli-width,2.1.0,ISC
-clipboard,1.7.1,MIT
-cliui,2.1.0,ISC
+clipboard,1.6.1,MIT
+cliui,3.2.0,ISC
clone,1.0.2,MIT
co,4.6.0,MIT
-coa,1.0.4,MIT
+coa,1.0.1,MIT
code-point-at,1.1.0,MIT
coercible,1.0.0,MIT
-coffee-rails,4.1.1,MIT
-coffee-script,2.4.1,MIT
-coffee-script-source,1.10.0,MIT
color,0.11.4,MIT
color-convert,1.9.0,MIT
color-name,1.1.2,MIT
@@ -221,20 +217,20 @@ colormin,1.1.2,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
combined-stream,1.0.5,MIT
-commander,2.11.0,MIT
+commander,2.9.0,MIT
commondir,1.0.1,MIT
component-bind,1.0.0,unknown
component-emitter,1.2.1,MIT
component-inherit,0.0.3,unknown
-compressible,2.0.10,MIT
+compressible,2.0.11,MIT
compression,1.7.0,MIT
-compression-webpack-plugin,0.3.2,MIT
+compression-webpack-plugin,1.0.0,MIT
concat-map,0.0.1,MIT
concat-stream,1.6.0,MIT
concurrent-ruby-ext,1.0.5,MIT
config-chain,1.1.11,MIT
configstore,1.4.0,Simplified BSD
-connect,3.6.2,MIT
+connect,3.6.3,MIT
connect-history-api-fallback,1.3.0,MIT
connection_pool,2.2.1,MIT
console-browserify,1.1.0,MIT
@@ -244,22 +240,25 @@ constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
content-disposition,0.5.2,MIT
content-type,1.0.2,MIT
-convert-source-map,1.5.0,MIT
+convert-source-map,1.3.0,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
+copy-webpack-plugin,4.0.1,MIT
core-js,2.4.1,MIT
core-util-is,1.0.2,MIT
-cosmiconfig,2.1.3,MIT
+cosmiconfig,2.1.1,MIT
crack,0.4.3,MIT
create-ecdh,4.0.0,MIT
-create-hash,1.1.3,MIT
-create-hmac,1.1.6,MIT
+create-hash,1.1.2,MIT
+create-hmac,1.1.4,MIT
creole,0.5.0,ruby
+cropper,2.3.0,MIT
+cross-spawn,5.1.0,MIT
cryptiles,2.0.5,New BSD
crypto-browserify,3.11.0,MIT
css-color-names,0.0.4,MIT
-css-loader,0.28.4,MIT
-css-selector-tokenizer,"",unknown
+css-loader,0.28.0,MIT
+css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
cssnano,3.10.0,MIT
@@ -267,7 +266,7 @@ csso,2.3.2,MIT
currently-unhandled,0.4.1,MIT
custom-event,1.0.1,MIT
d,1.0.0,MIT
-d3,3.5.17,New BSD
+d3,3.5.11,New BSD
d3_rails,3.5.11,MIT
dashdash,1.14.1,MIT
date-now,0.1.4,MIT
@@ -276,18 +275,18 @@ debug,2.6.8,MIT
debugger-ruby_core_source,1.3.8,MIT
decamelize,1.2.0,MIT
deckar01-task_list,2.0.0,MIT
+decompress-response,3.3.0,MIT
deep-equal,1.0.1,MIT
deep-extend,0.4.2,MIT
deep-is,0.1.3,MIT
default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
-defaults,1.0.3,MIT
defined,1.0.0,MIT
del,2.2.2,MIT
delayed-stream,1.0.0,MIT
-delegate,3.1.3,MIT
+delegate,3.1.2,MIT
delegates,1.0.0,MIT
-depd,1.1.0,MIT
+depd,1.1.1,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
destroy,1.0.4,MIT
@@ -296,48 +295,49 @@ detect-node,2.0.3,ISC
devise,4.2.0,MIT
devise-two-factor,3.0.0,MIT
di,0.0.1,MIT
-diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2"
+diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
dns-equal,1.0.0,MIT
-dns-packet,1.1.1,MIT
+dns-packet,1.2.2,MIT
dns-txt,2.0.2,MIT
doctrine,2.0.0,Apache 2.0
-document-register-element,1.5.0,MIT
+document-register-element,1.3.0,MIT
dom-serialize,2.2.1,MIT
dom-serializer,0.1.0,MIT
domain-browser,1.1.7,MIT
domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
domelementtype,1.3.0,unknown
-domhandler,2.4.1,Simplified BSD
-domutils,1.6.2,Simplified BSD
+domhandler,2.3.0,unknown
+domutils,1.5.1,unknown
doorkeeper,4.2.0,MIT
doorkeeper-openid_connect,1.1.2,MIT
-dropzone,4.3.0,MIT
+dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
duplexer,0.1.1,MIT
-duplexify,3.5.0,MIT
+duplexer3,0.1.4,New BSD
+duplexify,3.5.1,MIT
ecc-jsbn,0.1.1,MIT
editorconfig,0.13.2,MIT
ee-first,1.1.1,MIT
ejs,2.5.6,Apache 2.0
-electron-to-chromium,1.3.15,ISC
-elliptic,6.4.0,MIT
+electron-to-chromium,1.3.3,ISC
+elliptic,6.3.3,MIT
email_reply_trimmer,0.1.6,MIT
emoji-unicode-version,0.2.1,MIT
emojis-list,2.1.0,MIT
encodeurl,1.0.1,MIT
encryptor,3.0.0,MIT
-end-of-stream,1.0.0,MIT
+end-of-stream,1.4.0,MIT
engine.io,1.8.3,MIT
engine.io-client,1.8.3,MIT
engine.io-parser,1.3.2,MIT
-enhanced-resolve,3.3.0,MIT
+enhanced-resolve,3.4.1,MIT
ent,2.2.0,MIT
entities,1.1.1,BSD-like
equalizer,0.0.11,MIT
errno,0.1.4,MIT
-error-ex,1.3.1,MIT
+error-ex,1.3.0,MIT
erubis,2.7.0,MIT
es5-ext,0.10.24,MIT
es6-iterator,2.0.1,MIT
@@ -345,7 +345,7 @@ es6-map,0.1.5,MIT
es6-promise,3.0.2,MIT
es6-set,0.1.5,MIT
es6-symbol,3.1.1,MIT
-es6-weak-map,2.0.2,MIT
+es6-weak-map,2.0.1,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
escape_utils,1.1.1,MIT
@@ -353,19 +353,19 @@ escodegen,1.8.1,Simplified BSD
escope,3.6.0,Simplified BSD
eslint,3.19.0,MIT
eslint-config-airbnb-base,10.0.1,MIT
-eslint-import-resolver-node,0.3.1,MIT
+eslint-import-resolver-node,0.2.3,MIT
eslint-import-resolver-webpack,0.8.3,MIT
-eslint-module-utils,2.1.1,MIT
-eslint-plugin-filenames,1.2.0,MIT
-eslint-plugin-html,2.0.3,ISC
-eslint-plugin-import,2.7.0,MIT
-eslint-plugin-jasmine,2.7.1,MIT
+eslint-module-utils,2.0.0,MIT
+eslint-plugin-filenames,1.1.0,MIT
+eslint-plugin-html,2.0.1,ISC
+eslint-plugin-import,2.2.0,MIT
+eslint-plugin-jasmine,2.2.0,MIT
eslint-plugin-promise,3.5.0,ISC
-espree,3.4.3,Simplified BSD
-esprima,2.7.3,Simplified BSD
+espree,3.5.0,Simplified BSD
+esprima,4.0.0,Simplified BSD
esquery,1.0.0,BSD
-esrecurse,4.2.0,Simplified BSD
-estraverse,4.2.0,Simplified BSD
+esrecurse,4.1.0,Simplified BSD
+estraverse,4.1.1,Simplified BSD
esutils,2.0.2,BSD
et-orbi,1.0.3,MIT
etag,1.8.0,MIT
@@ -376,14 +376,15 @@ eventemitter3,1.2.0,MIT
events,1.1.1,MIT
eventsource,0.1.6,MIT
evp_bytestokey,1.0.0,MIT
-excon,0.55.0,MIT
+excon,0.57.1,MIT
+execa,0.7.0,MIT
execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
expand-braces,0.1.2,MIT
expand-brackets,0.1.5,MIT
expand-range,1.8.2,MIT
exports-loader,0.6.4,MIT
-express,4.15.3,MIT
+express,4.15.4,MIT
expression_parser,0.9.0,MIT
extend,3.0.1,MIT
extglob,0.3.2,MIT
@@ -392,40 +393,41 @@ extsprintf,1.0.2,MIT
faraday,0.12.1,MIT
faraday_middleware,0.11.0.1,MIT
faraday_middleware-multi_json,0.0.6,MIT
-fast-deep-equal,1.0.0,MIT
+fast-deep-equal,0.1.0,MIT
fast-levenshtein,2.0.6,MIT
fast_gettext,1.4.0,"MIT,ruby"
fastparse,1.1.1,MIT
faye-websocket,0.7.3,MIT
-ffi,1.9.10,BSD
+ffi,1.9.18,New BSD
figures,1.7.0,MIT
file-entry-cache,2.0.0,MIT
-file-loader,0.11.2,MIT
-filename-regex,2.0.1,MIT
+file-loader,0.11.1,MIT
+filename-regex,2.0.0,MIT
fileset,2.0.3,MIT
filesize,3.3.0,New BSD
fill-range,2.2.3,MIT
-finalhandler,1.0.3,MIT
-find-cache-dir,0.1.1,MIT
+finalhandler,1.0.4,MIT
+find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
-find-up,1.1.2,MIT
+find-up,2.1.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.10.2,MIT
flipper-active_record,0.10.2,MIT
flowdock,0.7.1,MIT
fog-aliyun,0.1.0,MIT
-fog-aws,0.13.0,MIT
-fog-core,1.44.1,MIT
-fog-google,0.5.0,MIT
+fog-aws,1.4.0,MIT
+fog-core,1.44.3,MIT
+fog-google,0.5.3,MIT
fog-json,1.0.2,MIT
-fog-local,0.3.0,MIT
-fog-openstack,0.1.6,MIT
+fog-local,0.3.1,MIT
+fog-openstack,0.1.21,MIT
fog-rackspace,0.1.1,MIT
fog-xml,0.1.3,MIT
+follow-redirects,1.2.3,MIT
font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
-for-in,1.0.2,MIT
-for-own,0.1.5,MIT
+for-in,0.1.6,MIT
+for-own,0.1.4,MIT
forever-agent,0.6.1,Apache 2.0
form-data,2.1.4,MIT
formatador,0.2.5,MIT
@@ -433,6 +435,7 @@ forwarded,0.1.0,MIT
fresh,0.5.0,MIT
from,0.1.7,MIT
fs-access,1.0.1,MIT
+fs-extra,0.26.7,MIT
fs.realpath,1.0.0,ISC
fsevents,1.1.2,MIT
fstream,1.0.11,ISC
@@ -440,45 +443,50 @@ fstream-ignore,1.0.5,ISC
function-bind,1.1.0,MIT
gauge,2.7.4,ISC
gemnasium-gitlab-service,0.2.6,MIT
-gemojione,3.0.1,MIT
+gemojione,3.3.0,MIT
generate-function,2.0.0,MIT
generate-object-property,1.2.0,MIT
get-caller-file,1.0.2,ISC
get-stdin,4.0.1,MIT
+get-stream,3.0.0,MIT
get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.2.0,MIT
-gitaly,0.14.0,MIT
+gitaly-proto,0.33.0,MIT
github-linguist,4.7.6,MIT
-github-markup,1.4.0,MIT
+github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
gitlab-grit,2.8.1,MIT
gitlab-markup,1.5.1,MIT
-gitlab_omniauth-ldap,1.2.1,MIT
-glob,7.1.2,ISC
+gitlab_omniauth-ldap,2.0.4,MIT
+glob,6.0.4,ISC
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
globalid,0.3.7,MIT
globals,9.18.0,MIT
globby,5.0.0,MIT
gollum-grit_adapter,1.0.1,MIT
-gollum-lib,4.2.1,MIT
+gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.8.7,Apache 2.0
-google-protobuf,3.2.0.2,New BSD
+google-protobuf,3.4.0.2,New BSD
googleauth,0.5.1,Apache 2.0
-got,3.3.1,MIT
+got,7.1.0,MIT
+gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
-grape,0.19.1,MIT
+graceful-readlink,1.0.1,MIT
+grape,1.0.0,MIT
grape-entity,0.6.0,MIT
-grpc,1.4.0,New BSD
+grape-route-helpers,2.1.0,MIT
+grape_logging,1.6.0,MIT
+grpc,1.4.5,New BSD
gzip-size,3.0.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
-handlebars,4.0.10,MIT
+handlebars,4.0.6,MIT
har-schema,1.0.5,ISC
har-validator,4.2.1,ISC
has,1.0.1,MIT
@@ -486,20 +494,20 @@ has-ansi,2.0.0,MIT
has-binary,0.1.7,MIT
has-cors,1.1.0,MIT
has-flag,2.0.0,MIT
+has-symbol-support-x,1.3.0,MIT
+has-to-string-tag-x,1.3.0,MIT
has-unicode,2.0.1,ISC
-hash-base,2.0.2,MIT
hash-sum,1.0.2,MIT
-hash.js,1.1.3,MIT
-hashie,3.5.5,MIT
+hash.js,1.0.3,MIT
+hashie,3.5.6,MIT
hashie-forbidden_attributes,0.1.1,MIT
hawk,3.1.3,New BSD
he,1.1.1,MIT
health_check,2.6.0,MIT
hipchat,1.5.2,MIT
-hmac-drbg,1.0.1,MIT
hoek,2.16.3,New BSD
home-or-tmp,2.0.0,MIT
-hosted-git-info,2.5.0,ISC
+hosted-git-info,2.2.0,ISC
hpack.js,2.1.6,MIT
html-comment-regex,1.1.1,MIT
html-entities,1.2.0,MIT
@@ -510,7 +518,7 @@ htmlparser2,3.9.2,MIT
http,0.9.8,MIT
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
-http-errors,1.6.1,MIT
+http-errors,1.6.2,MIT
http-form_data,1.0.1,MIT
http-proxy,1.16.2,MIT
http-proxy-middleware,0.17.4,MIT
@@ -519,15 +527,15 @@ http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
httpclient,2.8.2,ruby
https-browserify,0.0.1,MIT
-i18n,0.8.1,MIT
+i18n,0.8.6,MIT
ice_nine,0.11.2,MIT
iconv-lite,0.4.15,MIT
-icss-replace-symbols,1.1.0,ISC
-icss-utils,2.1.0,ISC
+icss-replace-symbols,1.0.2,ISC
ieee754,1.1.8,New BSD
ignore,3.3.3,MIT
ignore-by-default,1.0.1,ISC
immediate,3.0.6,MIT
+imports-loader,0.7.1,MIT
imurmurhash,0.1.4,MIT
indent-string,2.1.0,MIT
indexes-of,1.0.1,MIT
@@ -539,11 +547,11 @@ inherits,2.0.3,ISC
ini,1.3.4,ISC
inquirer,0.12.0,MIT
internal-ip,1.2.0,MIT
-interpret,1.0.3,MIT
+interpret,1.0.1,MIT
invariant,2.2.2,New BSD
invert-kv,1.0.0,MIT
ip,1.1.5,MIT
-ipaddr.js,1.3.0,MIT
+ipaddr.js,1.4.0,MIT
ipaddress,0.8.3,MIT
is-absolute,0.2.6,MIT
is-absolute-url,2.1.0,MIT
@@ -551,17 +559,17 @@ is-arrayish,0.2.1,MIT
is-binary-path,1.0.1,MIT
is-buffer,1.1.5,MIT
is-builtin-module,1.0.0,MIT
-is-directory,0.3.1,MIT
-is-dotfile,1.0.3,MIT
+is-dotfile,1.0.2,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
-is-extglob,1.0.0,MIT
+is-extglob,2.1.1,MIT
is-finite,1.0.2,MIT
-is-fullwidth-code-point,1.0.0,MIT
-is-glob,2.0.1,MIT
+is-fullwidth-code-point,2.0.0,MIT
+is-glob,3.1.0,MIT
is-my-json-valid,2.16.0,MIT
is-npm,1.0.0,MIT
is-number,2.1.0,MIT
+is-object,1.0.1,MIT
is-path-cwd,1.0.0,MIT
is-path-in-cwd,1.0.0,MIT
is-path-inside,1.0.0,MIT
@@ -572,6 +580,7 @@ is-property,1.0.2,MIT
is-redirect,1.0.0,MIT
is-relative,0.2.1,MIT
is-resolvable,1.0.0,MIT
+is-retry-allowed,1.1.0,MIT
is-stream,1.1.0,MIT
is-svg,2.1.0,MIT
is-typedarray,1.0.0,MIT
@@ -580,58 +589,64 @@ is-utf8,0.2.1,MIT
is-windows,0.2.0,MIT
isarray,1.0.0,MIT
isbinaryfile,3.0.2,MIT
-isexe,2.0.0,ISC
+isexe,1.1.2,ISC
isobject,2.1.0,MIT
isstream,0.1.2,MIT
istanbul,0.4.5,New BSD
-istanbul-api,1.1.10,New BSD
-istanbul-lib-coverage,1.1.1,New BSD
-istanbul-lib-hook,1.0.7,New BSD
-istanbul-lib-instrument,1.7.3,New BSD
-istanbul-lib-report,1.1.1,New BSD
-istanbul-lib-source-maps,1.2.1,New BSD
-istanbul-reports,1.1.1,New BSD
-jasmine-core,2.6.4,MIT
+istanbul-api,1.1.1,New BSD
+istanbul-lib-coverage,1.0.1,New BSD
+istanbul-lib-hook,1.0.0,New BSD
+istanbul-lib-instrument,1.4.2,New BSD
+istanbul-lib-report,1.0.0-alpha.3,New BSD
+istanbul-lib-source-maps,1.1.0,New BSD
+istanbul-reports,1.0.1,New BSD
+isurl,1.0.0,MIT
+jasmine-core,2.6.3,MIT
jasmine-jquery,2.1.1,MIT
jed,1.1.1,MIT
-jira-ruby,1.1.2,MIT
+jira-ruby,1.4.1,MIT
jodid25519,1.0.2,MIT
-jquery,2.2.4,MIT
+jquery,2.2.1,MIT
jquery-atwho-rails,1.3.2,MIT
jquery-rails,4.1.1,MIT
-jquery-ujs,1.2.2,MIT
+jquery-ujs,1.2.1,MIT
js-base64,2.1.9,BSD
-js-beautify,1.6.14,MIT
-js-cookie,2.1.4,MIT
-js-tokens,3.0.2,MIT
-js-yaml,"",unknown
+js-beautify,1.6.12,MIT
+js-cookie,2.1.3,MIT
+js-tokens,3.0.1,MIT
+js-yaml,3.7.0,MIT
jsbn,0.1.1,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
json-jwt,1.7.1,MIT
-json-loader,0.5.4,MIT
+json-loader,0.5.7,MIT
json-schema,0.2.3,"AFLv2.1,BSD"
json-schema-traverse,0.3.1,MIT
json-stable-stringify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
json3,3.3.2,MIT
json5,0.5.1,MIT
+jsonfile,2.4.0,MIT
jsonify,0.0.0,Public Domain
jsonpointer,4.0.1,MIT
jsprim,1.4.0,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
jszip-utils,0.0.2,MIT or GPLv3
jwt,1.5.6,MIT
-kaminari,0.17.0,MIT
+kaminari,1.0.1,MIT
+kaminari-actionview,1.0.1,MIT
+kaminari-activerecord,1.0.1,MIT
+kaminari-core,1.0.1,MIT
karma,1.7.0,MIT
-karma-chrome-launcher,2.2.0,MIT
-karma-coverage-istanbul-reporter,0.2.3,MIT
+karma-chrome-launcher,2.1.1,MIT
+karma-coverage-istanbul-reporter,0.2.0,MIT
karma-jasmine,1.1.0,MIT
-karma-mocha-reporter,2.2.3,MIT
+karma-mocha-reporter,2.2.2,MIT
karma-sourcemap-loader,0.3.7,MIT
karma-webpack,2.0.4,MIT
kgio,2.10.0,LGPL-2.1+
-kind-of,3.2.2,MIT
+kind-of,3.1.0,MIT
+klaw,1.3.1,MIT
kubeclient,2.2.0,MIT
latest-version,1.0.1,MIT
launchy,2.4.3,ISC
@@ -641,9 +656,9 @@ levn,0.3.0,MIT
licensee,8.7.0,MIT
lie,3.1.1,MIT
little-plugger,1.1.4,MIT
-load-json-file,1.1.0,MIT
+load-json-file,2.0.0,MIT
loader-runner,2.3.0,MIT
-loader-utils,"",unknown
+loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
lodash,4.17.4,MIT
@@ -657,20 +672,24 @@ lodash._isiterateecall,3.0.9,MIT
lodash._topath,3.8.1,MIT
lodash.assign,3.2.0,MIT
lodash.camelcase,4.3.0,MIT
+lodash.capitalize,4.2.1,MIT
lodash.cond,4.5.2,MIT
+lodash.deburr,4.1.0,MIT
lodash.defaults,3.1.2,MIT
-lodash.get,3.7.0,MIT
+lodash.get,4.4.2,MIT
lodash.isarguments,3.1.0,MIT
lodash.isarray,3.0.4,MIT
-lodash.kebabcase,4.1.1,MIT
+lodash.kebabcase,4.0.1,MIT
lodash.keys,3.1.2,MIT
lodash.memoize,4.1.2,MIT
lodash.restparam,3.6.1,MIT
-lodash.snakecase,4.1.1,MIT
+lodash.snakecase,4.0.1,MIT
lodash.uniq,4.5.0,MIT
-lodash.upperfirst,4.3.1,MIT
+lodash.words,4.2.0,MIT
log4js,0.6.38,Apache 2.0
logging,2.2.2,MIT
+loglevel,1.4.1,MIT
+lograge,0.5.1,MIT
longest,1.0.1,MIT
loofah,2.0.3,MIT
loose-envify,1.3.1,MIT
@@ -678,13 +697,15 @@ loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
lru-cache,3.2.0,ISC
macaddress,0.2.8,MIT
-mail,2.6.5,MIT
+mail,2.6.6,MIT
mail_room,0.9.1,MIT
+make-dir,1.0.0,MIT
map-obj,1.0.1,MIT
map-stream,0.1.0,unknown
marked,0.3.6,MIT
-math-expression-evaluator,1.2.17,MIT
+math-expression-evaluator,1.2.16,MIT
media-typer,0.3.0,MIT
+mem,1.1.0,MIT
memoist,0.15.0,MIT
memory-fs,0.4.1,MIT
meow,3.7.0,MIT
@@ -693,52 +714,53 @@ method_source,0.8.2,MIT
methods,1.1.2,MIT
micromatch,2.3.11,MIT
miller-rabin,4.0.0,MIT
-mime,1.3.6,MIT
+mime,1.3.4,MIT
mime-db,1.27.0,MIT
-mime-types,2.1.15,MIT
mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
mimemagic,0.3.0,MIT
-mini_portile2,2.1.0,MIT
+mimic-fn,1.1.0,MIT
+mimic-response,1.0.0,MIT
+mini_portile2,2.2.0,MIT
minimalistic-assert,1.0.0,ISC
-minimalistic-crypto-utils,1.0.1,MIT
-minimatch,3.0.4,ISC
+minimatch,3.0.3,ISC
minimist,0.0.8,MIT
mkdirp,0.5.1,MIT
mmap2,2.2.7,ruby
-moment,2.18.1,MIT
-mousetrap,1.6.1,Apache 2.0
+moment,2.17.1,MIT
+monaco-editor,0.8.3,MIT
+mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
ms,2.0.0,MIT
-msgpack,1.1.0,Apache 2.0
multi_json,1.12.1,MIT
multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
multipart-post,2.0.0,MIT
-mustermann,0.4.0,MIT
-mustermann-grape,0.4.0,MIT
+mustermann,1.0.0,MIT
+mustermann-grape,1.0.0,MIT
mute-stream,0.0.5,ISC
+mysql2,0.4.5,MIT
name-all-modules-plugin,1.0.1,MIT
nan,2.6.2,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
nested-error-stacks,1.0.2,MIT
-net-ldap,0.12.1,MIT
-net-ssh,3.0.1,MIT
+net-ldap,0.16.0,MIT
+net-ssh,4.1.0,MIT
netrc,0.11.0,MIT
-node-ensure,0.0.0,MIT
+node-dir,0.1.17,MIT
node-forge,0.6.33,BSD
node-libs-browser,2.0.0,MIT
node-pre-gyp,0.6.36,New BSD
-node-zopfli,2.0.2,MIT
nodemon,1.11.0,MIT
-nokogiri,1.6.8.1,MIT
-nopt,4.0.1,ISC
+nokogiri,1.8.0,MIT
+nopt,3.0.6,ISC
normalize-package-data,2.4.0,Simplified BSD
normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
-npmlog,4.1.2,ISC
+npm-run-path,2.0.2,MIT
+npmlog,4.1.0,ISC
null-check,1.0.0,MIT
num2fraction,1.2.2,MIT
number-is-nan,1.0.1,MIT
@@ -754,13 +776,13 @@ octokit,4.6.2,MIT
oj,2.17.5,MIT
omniauth,1.4.2,MIT
omniauth-auth0,1.4.1,MIT
-omniauth-authentiq,0.3.0,MIT
+omniauth-authentiq,0.3.1,MIT
omniauth-azure-oauth2,0.0.6,MIT
-omniauth-cas3,1.1.3,MIT
+omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
omniauth-github,1.1.2,MIT
omniauth-gitlab,1.0.2,MIT
-omniauth-google-oauth2,0.4.1,MIT
+omniauth-google-oauth2,0.5.2,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
omniauth-oauth,1.1.0,MIT
@@ -785,16 +807,19 @@ orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
os-homedir,1.0.2,MIT
-os-locale,1.4.0,MIT
+os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.4,ISC
+p-cancelable,0.3.0,MIT
+p-finally,1.0.0,MIT
p-limit,1.1.0,MIT
p-locate,2.0.0,MIT
p-map,1.1.1,MIT
+p-timeout,1.2.0,MIT
package-json,1.2.0,MIT
pako,1.0.5,(MIT AND Zlib)
paranoia,2.3.1,MIT
-parse-asn1,5.1.0,ISC
+parse-asn1,5.0.0,ISC
parse-glob,3.0.4,MIT
parse-json,2.2.0,MIT
parsejson,0.0.3,MIT
@@ -802,19 +827,20 @@ parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
parseurl,1.3.1,MIT
path-browserify,0.0.0,MIT
-path-exists,2.1.0,MIT
+path-exists,3.0.0,MIT
path-is-absolute,1.0.1,MIT
path-is-inside,1.0.2,(WTFPL OR MIT)
+path-key,2.0.1,MIT
path-parse,1.0.5,MIT
path-to-regexp,0.1.7,MIT
-path-type,1.1.0,MIT
+path-type,2.0.0,MIT
pause-stream,0.0.11,"MIT,Apache2"
-pbkdf2,3.0.12,MIT
-pdfjs-dist,1.8.527,Apache 2.0
+pbkdf2,3.0.9,MIT
peek,1.0.1,MIT
peek-gc,0.0.2,MIT
peek-host,1.0.0,MIT
-peek-performance_bar,1.2.1,MIT
+peek-mysql2,1.1.0,MIT
+peek-performance_bar,1.3.0,MIT
peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
@@ -822,15 +848,16 @@ peek-sidekiq,1.0.3,MIT
performance-now,0.2.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
pify,2.3.0,MIT
-pikaday,1.6.1,(0BSD OR MIT)
+pikaday,1.5.1,"BSD,MIT"
pinkie,2.0.4,MIT
pinkie-promise,2.0.1,MIT
-pkg-dir,1.0.0,MIT
+pkg-dir,2.0.0,MIT
+pkg-up,1.0.0,MIT
pluralize,1.2.1,MIT
po_to_json,1.0.1,MIT
portfinder,1.0.13,MIT
-posix-spawn,0.3.11,"MIT,LGPL"
-postcss,5.2.17,MIT
+posix-spawn,0.3.13,MIT
+postcss,5.2.16,MIT
postcss-calc,5.3.1,MIT
postcss-colormin,2.2.2,MIT
postcss-convert-values,2.6.1,MIT
@@ -851,10 +878,10 @@ postcss-minify-font-values,1.0.5,MIT
postcss-minify-gradients,1.0.5,MIT
postcss-minify-params,1.2.2,MIT
postcss-minify-selectors,2.1.1,MIT
-postcss-modules-extract-imports,1.1.0,ISC
-postcss-modules-local-by-default,1.2.0,MIT
-postcss-modules-scope,1.1.0,ISC
-postcss-modules-values,1.3.0,ISC
+postcss-modules-extract-imports,1.0.1,ISC
+postcss-modules-local-by-default,1.1.1,MIT
+postcss-modules-scope,1.0.2,ISC
+postcss-modules-values,1.2.2,ISC
postcss-normalize-charset,1.1.1,MIT
postcss-normalize-url,3.0.8,MIT
postcss-ordered-values,2.2.3,MIT
@@ -873,12 +900,12 @@ prepend-http,1.0.4,MIT
preserve,0.2.0,MIT
prismjs,1.6.0,MIT
private,0.1.7,MIT
-process,0.11.10,MIT
+process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
progress,1.1.8,MIT
-prometheus-client-mmap,0.7.0.beta8,Apache 2.0
+prometheus-client-mmap,0.7.0.beta14,Apache 2.0
proto-list,1.2.4,ISC
-proxy-addr,1.1.4,MIT
+proxy-addr,1.1.5,MIT
prr,0.0.0,MIT
ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
@@ -887,12 +914,12 @@ punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
q,1.5.0,MIT
qjobs,1.1.5,MIT
-qs,6.4.0,New BSD
-query-string,4.3.4,MIT
+qs,6.5.0,New BSD
+query-string,4.3.2,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
-rack,1.6.5,MIT
+rack,1.6.8,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
rack-cors,0.4.0,MIT
@@ -908,21 +935,24 @@ rails-i18n,4.0.9,MIT
railties,4.2.8,MIT
rainbow,2.2.2,MIT
raindrops,0.18.0,LGPL-2.1+
-rake,10.5.0,MIT
-randomatic,1.1.7,MIT
-randombytes,2.0.5,MIT
+rake,12.0.0,MIT
+randomatic,1.1.6,MIT
+randombytes,2.0.3,MIT
range-parser,1.2.0,MIT
raphael,2.2.7,MIT
-raven-js,3.16.1,Simplified BSD
+raven-js,3.14.0,Simplified BSD
raw-body,2.2.0,MIT
raw-loader,0.5.1,MIT
+rbnacl,4.0.2,MIT
+rbnacl-libsodium,1.0.11,MIT
rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,4.2.2,ruby
+re2,1.1.1,New BSD
react-dev-utils,0.5.2,New BSD
read-all-stream,3.1.0,MIT
-read-pkg,1.1.0,MIT
-read-pkg-up,1.0.1,MIT
-readable-stream,2.3.3,MIT
+read-pkg,2.0.0,MIT
+read-pkg-up,2.0.0,MIT
+readable-stream,2.0.6,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
@@ -941,14 +971,14 @@ redis-store,1.2.0,MIT
reduce-css-calc,1.3.0,MIT
reduce-function-call,1.0.2,MIT
regenerate,1.3.2,MIT
-regenerator-runtime,0.10.5,MIT
-regenerator-transform,0.9.11,BSD
+regenerator-runtime,0.10.1,MIT
+regenerator-transform,0.9.8,BSD
regex-cache,0.4.3,MIT
-regexpu-core,"",unknown
+regexpu-core,2.0.0,MIT
registry-url,3.1.0,MIT
regjsgen,0.2.0,MIT
regjsparser,0.1.5,BSD
-remove-trailing-separator,1.0.2,ISC
+remove-trailing-separator,1.1.0,ISC
repeat-element,1.1.2,MIT
repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
@@ -959,7 +989,7 @@ require-from-string,1.2.1,MIT
require-main-filename,1.0.1,ISC
require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
-resolve,1.3.3,MIT
+resolve,1.2.0,MIT
resolve-from,1.0.1,MIT
responders,2.3.0,MIT
rest-client,2.0.0,MIT
@@ -968,19 +998,19 @@ retriable,1.4.1,MIT
right-align,0.1.3,MIT
rimraf,2.6.1,ISC
rinku,2.0.0,ISC
-ripemd160,2.0.1,MIT
+ripemd160,1.0.1,New BSD
rotp,2.1.2,MIT
-rouge,2.1.0,MIT
+rouge,2.2.1,MIT
rqrcode,0.7.0,MIT
rqrcode-rails3,0.1.7,MIT
ruby-fogbugz,0.2.1,MIT
ruby-prof,0.16.2,Simplified BSD
ruby-saml,1.4.1,MIT
ruby_parser,3.9.0,MIT
-rubyntlm,0.5.2,MIT
+rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.25.1.1,MIT
+rugged,0.26.0,MIT
run-async,0.1.0,MIT
rx-lite,3.1.2,Apache 2.0
safe-buffer,5.1.1,MIT
@@ -989,21 +1019,20 @@ sanitize,2.1.0,MIT
sass,3.4.22,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
-sax,1.2.4,ISC
-schema-utils,0.3.0,MIT
+sax,1.2.2,ISC
securecompare,1.0.0,MIT
seed-fu,2.3.6,MIT
select,1.1.2,MIT
select-hose,2.0.0,MIT
select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
-selfsigned,1.9.1,MIT
+selfsigned,1.10.1,MIT
semver,5.3.0,ISC
semver-diff,2.1.0,MIT
-send,0.15.3,MIT
+send,0.15.4,MIT
sentry-raven,2.5.3,Apache 2.0
serve-index,1.9.0,MIT
-serve-static,1.12.3,MIT
+serve-static,1.12.4,MIT
set-blocking,2.0.0,ISC
set-immediate-shim,1.0.1,MIT
setimmediate,1.0.5,MIT
@@ -1011,8 +1040,10 @@ setprototypeof,1.0.3,ISC
settingslogic,2.0.9,MIT
sexp_processor,4.9.0,MIT
sha.js,2.4.8,MIT
+shebang-command,1.2.0,MIT
+shebang-regex,1.0.0,MIT
shelljs,0.7.8,New BSD
-sidekiq,5.0.0,LGPL
+sidekiq,5.0.4,LGPL
sidekiq-cron,0.6.0,MIT
sidekiq-limit_fetch,3.4.0,MIT
sigmund,1.0.1,ISC
@@ -1030,9 +1061,9 @@ socket.io-parser,2.3.1,MIT
sockjs,0.3.18,MIT
sockjs-client,1.0.1,MIT
sort-keys,1.1.2,MIT
-source-list-map,0.1.8,MIT
+source-list-map,2.0.0,MIT
source-map,0.5.6,New BSD
-source-map-support,0.4.15,MIT
+source-map-support,0.4.11,MIT
spdx-correct,1.0.2,Apache 2.0
spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
spdx-license-ids,1.2.2,Unlicense
@@ -1043,62 +1074,61 @@ sprintf-js,1.0.3,New BSD
sprockets,3.7.1,MIT
sprockets-rails,3.2.0,MIT
sql.js,0.4.0,MIT
-sshpk,1.13.1,MIT
+sshpk,1.13.0,MIT
state_machines,0.4.0,MIT
state_machines-activemodel,0.4.0,MIT
state_machines-activerecord,0.4.0,MIT
-stats-webpack-plugin,0.4.3,MIT
statuses,1.3.1,MIT
stream-browserify,2.0.1,MIT
stream-combiner,0.0.4,MIT
-stream-http,2.7.2,MIT
+stream-http,2.6.3,MIT
stream-shift,1.0.0,MIT
strict-uri-encode,1.1.0,MIT
string-length,1.0.1,MIT
-string-width,1.0.2,MIT
-string_decoder,0.10.31,MIT
-stringex,2.5.2,MIT
+string-width,2.0.0,MIT
+string_decoder,1.0.3,MIT
+stringex,2.7.1,MIT
stringstream,0.0.5,MIT
strip-ansi,3.0.1,MIT
-strip-bom,2.0.0,MIT
+strip-bom,3.0.0,MIT
+strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
-supports-color,4.2.0,MIT
+supports-color,4.2.1,MIT
svgo,0.7.2,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
-tapable,0.2.6,MIT
+tapable,0.2.8,MIT
tar,2.2.1,ISC
tar-pack,3.4.0,Simplified BSD
temple,0.7.7,MIT
-test-exclude,4.1.1,ISC
+test-exclude,4.0.0,ISC
text,1.3.1,MIT
text-table,0.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
three,0.84.0,MIT
three-orbit-controls,82.1.0,MIT
-three-stl-loader,1.0.5,MIT
+three-stl-loader,1.0.4,MIT
through,2.3.8,MIT
thunky,0.1.0,unknown
tilt,2.0.6,MIT
timeago.js,2.0.5,MIT
-timed-out,2.0.0,MIT
-timers-browserify,2.0.2,MIT
+timed-out,4.0.1,MIT
+timers-browserify,2.0.4,MIT
timfel-krb5-auth,0.8.3,LGPL
-tiny-emitter,2.0.1,MIT
+tiny-emitter,1.1.0,MIT
tmp,0.0.31,MIT
to-array,0.1.4,MIT
to-arraybuffer,1.0.1,MIT
-to-fast-properties,1.0.3,MIT
+to-fast-properties,1.0.2,MIT
toml-rb,0.3.15,MIT
-tool,0.2.3,MIT
touch,1.0.0,ISC
tough-cookie,2.3.2,New BSD
traverse,0.6.6,MIT
trim-newlines,1.0.0,MIT
trim-right,1.0.1,MIT
-truncato,0.7.8,MIT
+truncato,0.7.10,MIT
tryit,1.0.3,MIT
tty-browserify,0.0.0,MIT
tunnel-agent,0.6.0,Apache 2.0
@@ -1106,17 +1136,17 @@ tweetnacl,0.14.5,Unlicense
type-check,0.3.2,MIT
type-is,1.6.15,MIT
typedarray,0.0.6,MIT
-tzinfo,1.2.2,MIT
+tzinfo,1.2.3,MIT
u2f,0.2.1,MIT
uglifier,2.7.2,MIT
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
+uglifyjs-webpack-plugin,0.4.6,MIT
uid-number,0.0.6,ISC
ultron,1.1.0,MIT
unc-path-regex,0.1.2,MIT
undefsafe,0.0.3,MIT / http://rem.mit-license.org
underscore,1.8.3,MIT
-underscore-rails,1.8.3,MIT
unf,0.1.4,BSD
unf_ext,0.0.7.2,MIT
unicorn,5.1.0,ruby
@@ -1127,15 +1157,17 @@ uniqs,2.0.0,MIT
unpipe,1.0.0,MIT
update-notifier,0.5.0,Simplified BSD
url,0.11.0,MIT
-url-loader,0.5.9,MIT
+url-loader,0.5.8,MIT
url-parse,1.0.5,MIT
+url-parse-lax,1.0.0,MIT
+url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
user-home,2.0.0,MIT
-useragent,2.2.0,MIT
+useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.0,MIT
-uuid,3.1.0,MIT
+uuid,3.0.1,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
vary,1.1.1,MIT
@@ -1147,32 +1179,33 @@ visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.3.4,MIT
-vue-hot-reload-api,2.1.0,MIT
+vue,2.2.6,MIT
+vue-hot-reload-api,2.0.11,MIT
vue-loader,11.3.4,MIT
-vue-resource,0.9.3,MIT
+vue-resource,1.3.4,MIT
vue-style-loader,2.0.5,MIT
-vue-template-compiler,2.3.4,MIT
-vue-template-es2015-compiler,1.5.3,MIT
+vue-template-compiler,2.2.6,MIT
+vue-template-es2015-compiler,1.5.1,MIT
+vuex,2.3.1,MIT
warden,1.2.6,MIT
-watchpack,1.3.1,MIT
+watchpack,1.4.0,MIT
wbuf,1.7.2,MIT
-webpack,2.6.1,MIT
+webpack,3.5.5,MIT
webpack-bundle-analyzer,2.8.2,MIT
webpack-dev-middleware,1.11.0,MIT
-webpack-dev-server,2.5.1,MIT
+webpack-dev-server,2.7.1,MIT
webpack-rails,0.9.10,MIT
-webpack-sources,0.1.5,MIT
+webpack-sources,1.0.1,MIT
+webpack-stats-plugin,0.1.5,MIT
websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
whet.extend,0.9.9,MIT
-which,1.2.14,ISC
-which-module,1.0.0,ISC
+which,1.2.12,ISC
+which-module,2.0.0,ISC
wide-align,1.1.2,ISC
wikicloth,0.8.1,MIT
window-size,0.1.0,MIT
wordwrap,0.0.2,MIT/X11
-worker-loader,0.8.1,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
@@ -1185,6 +1218,6 @@ xmlhttprequest-ssl,1.5.3,MIT
xtend,4.0.1,MIT
y18n,3.2.1,ISC
yallist,2.1.2,ISC
-yargs,3.10.0,MIT
-yargs-parser,4.2.1,ISC
+yargs,8.0.2,MIT
+yargs-parser,7.0.0,ISC
yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index de4a9ac4487..57644482b32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,14 +49,7 @@ ajv-keywords@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
-ajv@^4.7.0:
- version "4.11.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
-ajv@^4.9.1:
+ajv@^4.7.0, ajv@^4.9.1:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
dependencies:
@@ -64,11 +57,11 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.5:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
co "^4.6.0"
- fast-deep-equal "^0.1.0"
+ fast-deep-equal "^1.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
@@ -877,7 +870,7 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-balanced-match@^0.4.1, balanced-match@^0.4.2:
+balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -983,14 +976,7 @@ bootstrap-sass@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
-brace-expansion@^1.0.0:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9"
- dependencies:
- balanced-match "^0.4.1"
- concat-map "0.0.1"
-
-brace-expansion@^1.1.8:
+brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
dependencies:
@@ -1712,7 +1698,7 @@ decompress-response@^3.2.0:
dependencies:
mimic-response "^1.0.0"
-deep-equal@^1.0.1:
+deep-equal@^1.0.1, deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -1730,7 +1716,14 @@ default-require-extensions@^1.0.0:
dependencies:
strip-bom "^2.0.0"
-defined@^1.0.0:
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+defined@^1.0.0, defined@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -1841,7 +1834,7 @@ doctrine@^2.0.0:
esutils "^2.0.2"
isarray "^1.0.0"
-document-register-element@^1.3.0:
+document-register-element@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
@@ -2037,6 +2030,24 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
+es-abstract@^1.5.0:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
version "0.10.24"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
@@ -2429,9 +2440,9 @@ extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-fast-deep-equal@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d"
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-levenshtein@~2.0.4:
version "2.0.6"
@@ -2564,6 +2575,12 @@ follow-redirects@^1.2.3:
dependencies:
debug "^2.4.5"
+for-each@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@@ -2574,6 +2591,10 @@ for-own@^0.1.4:
dependencies:
for-in "^0.1.5"
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -2646,6 +2667,10 @@ function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+function-bind@^1.1.1, function-bind@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -2687,6 +2712,10 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git":
+ version "1.0.4"
+ resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#46c0a49cd43639948dfcc77a0f94d59deaad1e85"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -2720,7 +2749,7 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.1.1:
+glob@^7.0.0, glob@^7.1.1, glob@~7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -2742,11 +2771,7 @@ glob@^7.0.3, glob@^7.0.5:
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^9.0.0:
- version "9.14.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
-
-globals@^9.14.0:
+globals@^9.0.0, globals@^9.14.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -2888,7 +2913,7 @@ has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-has@^1.0.1:
+has@^1.0.1, has@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
@@ -3163,6 +3188,14 @@ is-builtin-module@^1.0.0:
dependencies:
builtin-modules "^1.0.0"
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
is-dotfile@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d"
@@ -3201,6 +3234,10 @@ is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
is-glob@^2.0.0, is-glob@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -3276,6 +3313,12 @@ is-redirect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
is-relative@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
@@ -3302,6 +3345,10 @@ is-svg@^2.0.0:
dependencies:
html-comment-regex "^1.1.0"
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -4087,7 +4134,7 @@ minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -4225,8 +4272,8 @@ node-libs-browser@^2.0.0:
vm-browserify "0.0.4"
node-pre-gyp@^0.6.36:
- version "0.6.36"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ version "0.6.37"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05"
dependencies:
mkdirp "^0.5.1"
nopt "^4.0.1"
@@ -4235,6 +4282,7 @@ node-pre-gyp@^0.6.36:
request "^2.81.0"
rimraf "^2.6.1"
semver "^5.3.0"
+ tape "^4.6.3"
tar "^2.2.1"
tar-pack "^3.4.0"
@@ -4356,6 +4404,14 @@ object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+object-inspect@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d"
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -5383,6 +5439,12 @@ resolve@^1.1.6, resolve@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+resolve@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -5390,6 +5452,12 @@ restore-cursor@^1.0.1:
exit-hook "^1.0.0"
onetime "^1.0.0"
+resumer@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759"
+ dependencies:
+ through "~2.3.4"
+
right-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
@@ -5805,6 +5873,14 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
+string.prototype.trim@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -5865,6 +5941,10 @@ supports-color@^4.2.1:
dependencies:
has-flag "^2.0.0"
+svg4everybody@2.1.9:
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
+
svgo@^0.7.0:
version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
@@ -5896,6 +5976,24 @@ tapable@^0.2.7:
version "0.2.8"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+tape@^4.6.3:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e"
+ dependencies:
+ deep-equal "~1.0.1"
+ defined "~1.0.0"
+ for-each "~0.3.2"
+ function-bind "~1.1.0"
+ glob "~7.1.2"
+ has "~1.0.1"
+ inherits "~2.0.3"
+ minimist "~1.2.0"
+ object-inspect "~1.3.0"
+ resolve "~1.4.0"
+ resumer "~0.0.0"
+ string.prototype.trim "~1.1.2"
+ through "~2.3.8"
+
tar-pack@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
@@ -5943,7 +6041,7 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"